From 40cdd9a4c2d6029178886b362da73f2eeda9ed0a Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:53:27 -0700 Subject: [PATCH] Initial commit: duck derive macros --- .gitignore | 2 + Cargo.lock | 1965 +++++++++++++++++++++++ Cargo.toml | 79 + README.md | 15 + crates/libduck-derive/Cargo.toml | 16 + crates/libduck-derive/src/duck_value.rs | 64 + crates/libduck-derive/src/from_duck.rs | 99 ++ crates/libduck-derive/src/lib.rs | 240 +++ crates/libduck-derive/src/to_duck.rs | 81 + crates/libduck-derive/src/util.rs | 128 ++ crates/libduck/Cargo.toml | 30 + crates/libduck/src/containers.rs | 392 +++++ crates/libduck/src/from_duck.rs | 159 ++ crates/libduck/src/lib.rs | 133 ++ crates/libduck/src/primitives.rs | 729 +++++++++ crates/libduck/src/special.rs | 87 + crates/libduck/src/to_duck.rs | 113 ++ rustfmt.toml | 1 + 18 files changed, 4333 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 crates/libduck-derive/Cargo.toml create mode 100644 crates/libduck-derive/src/duck_value.rs create mode 100644 crates/libduck-derive/src/from_duck.rs create mode 100644 crates/libduck-derive/src/lib.rs create mode 100644 crates/libduck-derive/src/to_duck.rs create mode 100644 crates/libduck-derive/src/util.rs create mode 100644 crates/libduck/Cargo.toml create mode 100644 crates/libduck/src/containers.rs create mode 100644 crates/libduck/src/from_duck.rs create mode 100644 crates/libduck/src/lib.rs create mode 100644 crates/libduck/src/primitives.rs create mode 100644 crates/libduck/src/special.rs create mode 100644 crates/libduck/src/to_duck.rs create mode 100644 rustfmt.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b88d6d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +*.ignore \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..471385a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1965 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" +dependencies = [ + "ahash 0.8.11", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.15.2", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ord" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-select" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" +dependencies = [ + "ahash 0.8.11", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "darling" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "duckdb" +version = "1.2.1" +source = "git+https://github.com/duckdb/duckdb-rs.git?rev=6ffcc70b4f1f67e19f3789b206cc22f4b8811468#6ffcc70b4f1f67e19f3789b206cc22f4b8811468" +dependencies = [ + "arrow", + "cast", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libduckdb-sys", + "memchr", + "num-integer", + "rust_decimal", + "serde_json", + "smallvec", + "strum", + "url", + "uuid", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lexical-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libduck" +version = "0.0.1" +dependencies = [ + "chrono", + "duckdb", + "itertools", + "libduck-derive", + "serde", + "serde_json", + "url", + "utoipa", + "uuid", +] + +[[package]] +name = "libduck-derive" +version = "0.0.1" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "libduckdb-sys" +version = "1.2.1" +source = "git+https://github.com/duckdb/duckdb-rs.git?rev=6ffcc70b4f1f67e19f3789b206cc22f4b8811468#6ffcc70b4f1f67e19f3789b206cc22f4b8811468" +dependencies = [ + "autocfg", + "cc", + "flate2", + "pkg-config", + "serde", + "serde_json", + "tar", + "vcpkg", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.101", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", + "sha1_smol", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70cd9c3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,79 @@ +[workspace] +members = ["crates/*"] +resolver = "2" + + +[workspace.package] +edition = "2024" +version = "0.0.1" + +[workspace.lints.rust] +unused_import_braces = "deny" +unit_bindings = "deny" +single_use_lifetimes = "deny" +non_ascii_idents = "deny" +macro_use_extern_crate = "deny" +elided_lifetimes_in_paths = "deny" +absolute_paths_not_starting_with_crate = "deny" +explicit_outlives_requirements = "warn" +unused_crate_dependencies = "warn" +redundant_lifetimes = "warn" +missing_docs = "warn" + +[workspace.lints.clippy] +upper_case_acronyms = "deny" +needless_return = "allow" +new_without_default = "allow" +tabs_in_doc_comments = "allow" +dbg_macro = "deny" +allow_attributes = "deny" +create_dir = "deny" +filetype_is_file = "deny" +integer_division = "allow" +lossy_float_literal = "deny" +map_err_ignore = "deny" +mutex_atomic = "deny" +needless_raw_strings = "deny" +str_to_string = "deny" +string_add = "deny" +string_to_string = "deny" +use_debug = "allow" +verbose_file_reads = "deny" +large_types_passed_by_value = "deny" +wildcard_dependencies = "deny" +negative_feature_names = "deny" +redundant_feature_names = "deny" +multiple_crate_versions = "allow" +missing_safety_doc = "warn" +identity_op = "allow" +print_stderr = "deny" +print_stdout = "deny" +comparison_chain = "allow" +unimplemented = "deny" +unwrap_used = "warn" +expect_used = "warn" + + +# +# MARK: dependencies +# + +[workspace.dependencies] +libduck-derive = { path = "crates/libduck-derive" } +libduck = { path = "crates/libduck" } + +uuid = { version = "1.16.0", features = ["serde", "v4", "v5"] } +url = { version = "2.5.4", features = ["serde"] } +chrono = { version = "0.4.40", features = ["serde"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +strum = { version = "0.27", features = ["derive"] } +itertools = "0.14.0" +utoipa = "5.4.0" + +proc-macro2 = "1.0.95" +syn = "2.0.101" +quote = "1.0.40" +paste = "1.0.15" +static_assertions = "1.1.0" +darling = "0.21.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6333c8 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +`libduck` provides quality-of-life features for [duckdb-rs](https://github.com/duckdb/duckdb-rs). See doc comments in `lib.rs` for details. + +## TODO: + +- `CAST(? AS JSON)` hack docs, feature, and example +- features (utoipa, url, etc) +- tests + - simple struct, all types + - u8/blob + - json + - option json and blob (Option must be external) + - nested structs + - string enums + - arc json, arc blob (repeated nesting) +- docstring examples (from tests) diff --git a/crates/libduck-derive/Cargo.toml b/crates/libduck-derive/Cargo.toml new file mode 100644 index 0000000..5f28f35 --- /dev/null +++ b/crates/libduck-derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libduck-derive" +version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true } +quote = { workspace = true } +proc-macro2 = { workspace = true } +darling = { workspace = true } diff --git a/crates/libduck-derive/src/duck_value.rs b/crates/libduck-derive/src/duck_value.rs new file mode 100644 index 0000000..4a10185 --- /dev/null +++ b/crates/libduck-derive/src/duck_value.rs @@ -0,0 +1,64 @@ +use quote::{ToTokens, quote}; +use syn::Fields; + +use crate::util::{DuckFieldMode, parse_attrs}; + +pub fn duck_value(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream { + let mut columns = Vec::new(); + + for f in fields { + let ident = &f.ident; + let ty = &f.ty; + let ident_str = ident.clone().to_token_stream().to_string(); + + let attrs = match parse_attrs(&f.attrs, ty) { + Err(x) => return x, + Ok(x) => x, + }; + + match attrs.mode { + DuckFieldMode::Json => { + if let Some(inty) = attrs.optional_inner { + columns.push(quote! { + ( + #ident_str.to_owned(), + < + Option<::libduck::special::DuckJson<#inty>> + as ::libduck::DuckValue + >::duck_type() + ) + }); + } else { + columns.push(quote! { + ( + #ident_str.to_owned(), + < + ::libduck::special::DuckJson<#ty> + as ::libduck::DuckValue + >::duck_type() + ) + }); + } + } + + DuckFieldMode::Normal => { + columns.push(quote! { + ( + #ident_str.to_owned(), + <#ty as ::libduck::DuckValue>::duck_type() + ) + }); + } + } + } + + return quote! { + impl ::libduck::DuckValue for #name { + fn duck_type() -> ::libduck::duckdb::types::Type { + ::libduck::duckdb::types::Type::Struct( + vec![#(#columns),*] + ) + } + } + }; +} diff --git a/crates/libduck-derive/src/from_duck.rs b/crates/libduck-derive/src/from_duck.rs new file mode 100644 index 0000000..95eb720 --- /dev/null +++ b/crates/libduck-derive/src/from_duck.rs @@ -0,0 +1,99 @@ +use quote::{quote, ToTokens}; +use syn::Fields; + +use crate::util::{parse_attrs, DuckFieldMode}; + +pub fn from_duck(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream { + let mut lines = Vec::new(); + let mut idents = Vec::new(); + + for f in fields { + let ident = &f.ident; + let ty = &f.ty; + let ident_str = ident.clone().to_token_stream().to_string(); + + let attrs = match parse_attrs(&f.attrs, ty) { + Err(x) => return x, + Ok(x) => x, + }; + + match attrs.mode { + DuckFieldMode::Json => { + if let Some(inty) = attrs.optional_inner { + lines.push(quote! { + path_to_field.push(#ident_str.to_owned()); + let #ident = { + let value = stx.get(&#ident_str.to_owned()).cloned(); + < + Option<::libduck::special::DuckJson<#inty>> + as ::libduck::FromDuck + >::from_duck_with_path(value, &path_to_field)?.map(|x| x.0) + }; + path_to_field.pop(); + }); + } else { + lines.push(quote! { + path_to_field.push(#ident_str.to_owned()); + let #ident = { + let value = stx.get(&#ident_str.to_owned()).cloned(); + < + ::libduck::special::DuckJson<#ty> + as ::libduck::FromDuck + >::from_duck_with_path(value, &path_to_field)?.0 + }; + path_to_field.pop(); + }); + } + } + + DuckFieldMode::Normal => { + lines.push(quote! { + path_to_field.push(#ident_str.to_owned()); + let #ident = { + + // TODO: no clone + let value = stx.get(&#ident_str.to_owned()).cloned(); + <#ty as ::libduck::FromDuck>::from_duck_with_path(value, &path_to_field)? + }; + path_to_field.pop(); + }); + } + } + + idents.push(ident); + } + + return quote! { + impl ::libduck::FromDuck for #name { + fn from_duck_with_path(value: Option<::libduck::duckdb::types::Value>, path_to_field: &[String]) -> Result { + let mut path_to_field: Vec<_> = path_to_field.into(); + + match value { + Some(::libduck::duckdb::types::Value::Struct(stx)) => { + #(#lines)* + + return Ok(Self { + #(#idents),* + }); + } + + Some(x) => { + return Err(::libduck::FromDuckError::InvalidType { + path_to_field, + expected: ::duck_type(), + got: ::libduck::_infer_value_type(&x), + }); + } + + None => { + return Err(::libduck::FromDuckError::InvalidType { + path_to_field, + expected: ::duck_type(), + got: None, + }); + } + } + } + } + }; +} diff --git a/crates/libduck-derive/src/lib.rs b/crates/libduck-derive/src/lib.rs new file mode 100644 index 0000000..12f9cd8 --- /dev/null +++ b/crates/libduck-derive/src/lib.rs @@ -0,0 +1,240 @@ +//! Procedural macros for automatically deriving duck traits. +//! Do not use this crate directly, always import it using `libduck`'s re-export. +//! +//! This crate provides derive macros that implement [`FromDuck`], and [`ToDuck`], and [`DuckValue`]. +//! +//! See [`FromDuck`] for detailed docs. + +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +mod duck_value; +mod from_duck; +mod to_duck; +mod util; + +/// This macro derives [`FromDuck`] for structs and for enums with no fields. +/// - [`FromDuck`] requires [`DuckValue`], and you'll likely want to derive that too. +/// - Be careful with binary data. [`Vec`] maps to `TINYINT[]` in duckdb, while [`DuckBlob`] maps to `BLOB`. +/// +/// +/// # Struct example +/// +/// ```ignore +/// #[derive(Debug, Clone, DuckValue, FromDuck, ToDuck)] +/// pub struct User { +/// // These fields may not be null +/// pub uuid: Uuid, +/// pub name: String, +/// pub created_at: chrono::DateTime, +/// +/// // This field is optional, and may be null +/// pub bio: Option, +/// +/// // This field is stored as a json string +/// #[duck(json)] +/// pub extra: HashMap +/// +/// // This field is stored as a `MAP(TEXT, TEXT)` +/// pub tags: HashMap +/// +/// // This field is a binary blob. +/// // +/// // The `Arc` has no effect on the type of this field +/// // in duckdb, and is transparently (de)serialized. +/// pub avatar: Option> +/// } +/// ``` +/// +/// +/// # Enum example +/// +/// [`FromDuck`], [`ToDuck`], and [`DuckValue`] can only be derived for enums whose variants have no fields. +/// They are stored as TEXT in duckdb, and we use the enum's implementation of [`ToString`] and [`std::str::FromStr`] +/// to convert variants to/from strings. +/// +/// You may use [strum](https://docs.rs/strum/latest/strum) to derive [`ToString`] and [`std::str::FromStr`] +/// for string enums like this. +/// +/// ```ignore +/// #[derive(DuckValue, FromDuck, ToDuck)] +/// pub enum State { +/// A, +/// B, +/// C +/// } +/// `` +#[proc_macro_derive(FromDuck, attributes(duck))] +pub fn derive_fromduck(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = input.ident.clone(); + + match &input.data { + syn::Data::Struct(data) => return from_duck::from_duck(&name, &data.fields).into(), + + syn::Data::Enum(data) => { + if let Some(f) = data + .variants + .iter() + .map(|x| x.fields.iter().next()) + .find(|x| x.is_some()) + .flatten() + { + let error = syn::Error::new_spanned( + f, + "FromDuck can only be derived for enums with no fields", + ); + return error.to_compile_error().into(); + } + + let name_str = name.to_string(); + + return quote! { + impl ::libduck::FromDuck for #name { + fn from_duck_with_path( + value: Option<::libduck::duckdb::types::Value>, + path_to_field: &[String] + ) -> Result { + use std::str::FromStr; + + let v = match value { + Some(::libduck::duckdb::types::Value::Text(s)) => match <#name as FromStr>::from_str(&s) { + Ok(x) => x, + Err(error) => { + return Err(::libduck::FromDuckError::ParseError { + path_to_field: path_to_field.into(), + string: s.into(), + msg: Some(format!("invalid {}", #name_str)), + parent: Some(Box::new(error)) + }); + } + }, + + Some(x) => { + return Err(::libduck::FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: ::duck_type(), + got: ::libduck::_infer_value_type(&x), + }); + } + + None => { + return Err(::libduck::FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: ::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } + } + } + .into(); + } + + _ => { + let error = syn::Error::new_spanned( + &input, + "FromDuck can only be derived for structs and enums", + ); + return error.to_compile_error().into(); + } + }; +} + +/// This macro derives [`ToDuck`] for structs and for enums with no fields. +/// - [`ToDuck`] requires [`DuckValue`], and you'll likely want to derive that too. +/// +/// This macro works exactly like [`FromDuck`]. +/// Refer to that macro for detailed docs. +#[proc_macro_derive(ToDuck, attributes(duck))] +pub fn derive_toduck(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = input.ident.clone(); + + match &input.data { + syn::Data::Struct(data) => return to_duck::to_duck(&name, &data.fields).into(), + + syn::Data::Enum(data) => { + if let Some(f) = data + .variants + .iter() + .map(|x| x.fields.iter().next()) + .find(|x| x.is_some()) + .flatten() + { + let error = syn::Error::new_spanned( + f, + "ToDuck can only be derived for enums with no fields", + ); + return error.to_compile_error().into(); + } + + return quote! { + impl ::libduck::ToDuck for #name { + fn to_duck_with_path( + &self, + path_to_field: &[String] + ) -> Result<::libduck::duckdb::types::Value, ::libduck::ToDuckError> { + Ok(::libduck::duckdb::types::Value::Text(::to_string(self))) + } + } + } + .into(); + } + + _ => { + let error = + syn::Error::new_spanned(&input, "ToDuck can only be derived for structs and enums"); + return error.to_compile_error().into(); + } + }; +} + +/// This macro derives [`DuckValue`] for structs and for enums with no fields. +/// +/// Refer to [`FromDuck`] for detailed docs. +#[proc_macro_derive(DuckValue, attributes(duck))] +pub fn derive_duckvalue(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = input.ident.clone(); + + match &input.data { + syn::Data::Struct(data) => return duck_value::duck_value(&name, &data.fields).into(), + + syn::Data::Enum(data) => { + if let Some(f) = data + .variants + .iter() + .map(|x| x.fields.iter().next()) + .find(|x| x.is_some()) + .flatten() + { + let error = syn::Error::new_spanned( + f, + "DuckValue can only be derived for enums with no fields", + ); + return error.to_compile_error().into(); + } + + return quote! { + impl ::libduck::DuckValue for #name { + fn duck_type() -> ::libduck::duckdb::types::Type { + ::libduck::duckdb::types::Type::Text + } + } + } + .into(); + } + + _ => { + let error = syn::Error::new_spanned( + &input, + "DuckValue can only be derived for structs and enums", + ); + return error.to_compile_error().into(); + } + }; +} diff --git a/crates/libduck-derive/src/to_duck.rs b/crates/libduck-derive/src/to_duck.rs new file mode 100644 index 0000000..03bc79a --- /dev/null +++ b/crates/libduck-derive/src/to_duck.rs @@ -0,0 +1,81 @@ +use quote::{ToTokens, quote}; +use syn::Fields; + +use crate::util::{DuckFieldMode, parse_attrs}; + +pub fn to_duck(name: &proc_macro2::Ident, fields: &Fields) -> proc_macro2::TokenStream { + let mut lines = Vec::new(); + + for f in fields { + let ident = &f.ident; + let ty = &f.ty; + let ident_str = ident.clone().to_token_stream().to_string(); + + let attrs = match parse_attrs(&f.attrs, ty) { + Err(x) => return x, + Ok(x) => x, + }; + + match attrs.mode { + DuckFieldMode::Json => { + // Note how we use refs here to prevent cloning + + if let Some(inty) = attrs.optional_inner { + lines.push(quote! { + { + path_to_field.push(#ident_str.to_owned()); + + let val = self.#ident.as_ref().map(::libduck::special::DuckJson); + let #ident = < + Option<::libduck::special::DuckJson<&#inty>> + as ::libduck::ToDuck + >::to_duck_with_path(&val, &path_to_field)?; + + fields.push((#ident_str.to_owned(), #ident)); + path_to_field.pop(); + } + }); + } else { + lines.push(quote! { + { + path_to_field.push(#ident_str.to_owned()); + let val = ::libduck::special::DuckJson(&self.#ident); + let #ident = < + ::libduck::special::DuckJson<&#ty> + as ::libduck::ToDuck + >::to_duck_with_path(&val, &path_to_field)?; + + fields.push((#ident_str.to_owned(), #ident)); + path_to_field.pop(); + } + }); + } + } + + DuckFieldMode::Normal => { + lines.push(quote! { + { + path_to_field.push(#ident_str.to_owned()); + let #ident = <#ty as ::libduck::ToDuck>::to_duck_with_path(&self.#ident, &path_to_field)?; + fields.push((#ident_str.to_owned(), #ident)); + path_to_field.pop(); + } + }); + } + } + } + + return quote! { + impl ::libduck::ToDuck for #name { + fn to_duck_with_path(&self, path_to_field: &[String]) -> Result<::libduck::duckdb::types::Value, ::libduck::ToDuckError> { + let mut path_to_field: Vec<_> = path_to_field.into(); + let mut fields = Vec::new(); + + #(#lines);* + + let x = ::libduck::duckdb::types::OrderedMap::from(fields); + return Ok(::libduck::duckdb::types::Value::Struct(x)); + } + } + }; +} diff --git a/crates/libduck-derive/src/util.rs b/crates/libduck-derive/src/util.rs new file mode 100644 index 0000000..be505eb --- /dev/null +++ b/crates/libduck-derive/src/util.rs @@ -0,0 +1,128 @@ +use darling::FromMeta; +use quote::ToTokens; +use syn::{AttrStyle, Attribute, MacroDelimiter, Meta, MetaList, Path, Type}; + +pub enum DuckFieldMode { + Normal, + Json, +} + +pub struct DuckAttrs { + pub mode: DuckFieldMode, + + /// If some, this type is optional, + /// and the inner type is inside. + /// + /// We use this to strip the first option from fields + /// annotated with `#[duck(json)]`, since the option needs + /// to be placed _before_ `DuckJson`. + pub optional_inner: Option, +} + +/// Given tokens for `Option`, return `T`. +/// Returns `None` if this is not an `Option`. +pub fn option_inner(ty: &Type) -> Option<&Type> { + let syn::Type::Path(ty) = ty else { return None }; + if ty.qself.is_some() { + return None; + } + + let ty = &ty.path; + + #[expect(clippy::unwrap_used)] // We check length + if ty.segments.is_empty() || ty.segments.last().unwrap().ident != "Option" { + return None; + } + + if !(ty.segments.len() == 1 + || (ty.segments.len() == 3 + && ["core", "std"].contains(&ty.segments[0].ident.to_string().as_str()) + && ty.segments[1].ident == "option")) + { + return None; + } + + #[expect(clippy::unwrap_used)] // We checked length + let last_segment = ty.segments.last().unwrap(); + + let syn::PathArguments::AngleBracketed(generics) = &last_segment.arguments else { + return None; + }; + if generics.args.len() != 1 { + return None; + } + let syn::GenericArgument::Type(inner_type) = &generics.args[0] else { + return None; + }; + + Some(inner_type) +} + +/// Parse the `#[duck]` attribute +pub fn parse_attrs( + attrs: &Vec, + ty: &Type, +) -> Result { + #[derive(Debug, Default, FromMeta, PartialEq, Eq)] + #[darling(derive_syn_parse)] + struct RawDuckAttr { + #[darling(default)] + json: bool, + } + + impl RawDuckAttr { + fn merge(&mut self, rhs: Self) { + self.json |= rhs.json; + } + } + + let mut rda = RawDuckAttr::default(); + + for a in attrs { + match a { + Attribute { + style: AttrStyle::Outer, + meta: + Meta::List(MetaList { + delimiter: MacroDelimiter::Paren(_), + + tokens, + path: Path { + leading_colon: None, + segments, + }, + }), + .. + } => { + // Not our token + if segments.to_token_stream().to_string() != "duck" { + continue; + } + + let attr: RawDuckAttr = match syn::parse(tokens.clone().into()) { + Ok(x) => x, + Err(err) => { + let error = syn::Error::new_spanned(a, format!("{err}")); + return Err(error.to_compile_error()); + } + }; + + rda.merge(attr); + } + + _ => continue, + } + } + + let optional_inner = option_inner(ty).cloned(); + + let mode = match rda.json { + true => DuckFieldMode::Json, + false => DuckFieldMode::Normal, + }; + + return Ok(DuckAttrs { + mode, + optional_inner, + }); +} diff --git a/crates/libduck/Cargo.toml b/crates/libduck/Cargo.toml new file mode 100644 index 0000000..2113e4b --- /dev/null +++ b/crates/libduck/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "libduck" +version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[dependencies] +libduck-derive = { workspace = true } + +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +itertools = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } +utoipa = { workspace = true } + +# duckdb is only used by this crate, +# and re-exported everywhere else. + +# duckdb 1.2.1 is broken, https://github.com/duckdb/duckdb-rs/issues/467 +# 1.2.2 should fix those issues, but until then we must pull from git. +# duckdb = { version = "1.2.1", features = ["bundled", "parquet"] } + +[dependencies.duckdb] +git = "https://github.com/duckdb/duckdb-rs.git" +rev = "6ffcc70b4f1f67e19f3789b206cc22f4b8811468" +features = ["bundled", "parquet", "chrono", "json", "serde_json", "url", "uuid"] diff --git a/crates/libduck/src/containers.rs b/crates/libduck/src/containers.rs new file mode 100644 index 0000000..2c5f1a9 --- /dev/null +++ b/crates/libduck/src/containers.rs @@ -0,0 +1,392 @@ +use duckdb::types::{OrderedMap, Value}; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{Debug, Display}, + hash::Hash, + ops::Deref, + rc::Rc, + sync::Arc, +}; +use utoipa::ToSchema; + +use crate::{DuckValue, Type, _infer_value_type}; + +use super::{ + from_duck::{FromDuck, FromDuckError}, + to_duck::{ToDuck, ToDuckError}, +}; + +// TODO: Duration + +// MARK: Cow + +impl DuckValue for Cow<'_, T> { + #[inline] + fn duck_type() -> Type { + T::duck_type() + } +} + +impl FromDuck for Cow<'_, T> { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + Ok(Cow::Owned(T::from_duck_with_path(val, path_to_field)?)) + } +} + +impl ToDuck for Cow<'_, T> { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + self.deref().to_duck_with_path(_path_to_field) + } +} + +// MARK: Arc + +impl DuckValue for Arc { + #[inline] + fn duck_type() -> Type { + T::duck_type() + } +} + +impl FromDuck for Arc { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + Ok(Arc::new(T::from_duck_with_path(val, path_to_field)?)) + } +} + +impl ToDuck for Arc { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + self.deref().to_duck_with_path(_path_to_field) + } +} + +// MARK: Rc + +impl DuckValue for Rc { + #[inline] + fn duck_type() -> Type { + T::duck_type() + } +} + +impl FromDuck for Rc { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + Ok(Rc::new(T::from_duck_with_path(val, path_to_field)?)) + } +} + +impl ToDuck for Rc { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + self.deref().to_duck_with_path(_path_to_field) + } +} + +// MARK: Box + +impl DuckValue for Box { + #[inline] + fn duck_type() -> Type { + T::duck_type() + } +} + +impl FromDuck for Box { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + Ok(Box::new(T::from_duck_with_path(val, path_to_field)?)) + } +} + +impl ToDuck for Box { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + self.deref().to_duck_with_path(_path_to_field) + } +} + +// MARK: Option + +impl DuckValue for Option { + #[inline] + fn duck_type() -> Type { + T::duck_type() + } +} + +impl FromDuck for Option { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + match val { + None | Some(Value::Null) => return Ok(None), + x => return Ok(Some(T::from_duck_with_path(x, path_to_field)?)), + }; + } +} + +impl ToDuck for Option { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + match self { + Some(x) => return x.to_duck_with_path(_path_to_field), + None => return Ok(Value::Null), + }; + } +} + +// MARK: Vec + +impl DuckValue for Vec { + #[inline] + fn duck_type() -> Type { + Type::List(Box::new(T::duck_type())) + } +} + +impl FromDuck for Vec { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let mut path: Vec<_> = path_to_field.into(); + + match val { + Some(Value::List(x)) => { + let mut out = Vec::new(); + for v in x { + path.push(format!("{}", out.len())); + out.push(T::from_duck_with_path(Some(v), &path)?); + path.pop(); + } + return Ok(out); + } + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + } +} + +impl ToDuck for Vec { + #[inline] + fn to_duck_with_path(&self, path_to_field: &[String]) -> Result { + let mut path: Vec<_> = path_to_field.into(); + + let mut out = Vec::with_capacity(self.len()); + for x in self { + path.push(format!("{}", out.len())); + out.push(x.to_duck_with_path(&path)?); + path.pop(); + } + return Ok(Value::List(out)); + } +} + +// MARK: DuckBlob + +/// Binary data stored in DuckDB as a BLOB type. +/// +/// `DuckBlob` wraps a `Vec` and maps to DuckDB's BLOB type. +/// A plain `Vec` maps to `TINYINT[]` instead. +#[derive(Clone, Serialize, Deserialize, ToSchema)] +#[serde(transparent)] +#[schema(value_type = Vec)] +pub struct DuckBlob(pub Vec); + +impl Debug for DuckBlob { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DuckBlob") + .field("len", &self.0.len()) + .finish() + } +} + +impl DuckBlob { + /// Create a new [`DuckBlob`] containing the given vector + #[inline] + pub fn new(buffer: Vec) -> Self { + Self(buffer) + } + + /// Take the vector out of this [`DuckBlob`] + #[inline] + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl From> for DuckBlob { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: DuckBlob) -> Self { + value.0 + } +} + +impl Deref for DuckBlob { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DuckValue for DuckBlob { + #[inline] + fn duck_type() -> Type { + Type::Blob + } +} + +impl FromDuck for DuckBlob { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + match val { + Some(Value::Blob(x)) => Ok(DuckBlob(x)), + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + } + } +} + +impl ToDuck for DuckBlob { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + // TODO: no clone? + return Ok(Value::Blob(self.0.clone())); + } +} + +// MARK: HashMap + +impl DuckValue for HashMap { + #[inline] + fn duck_type() -> Type { + Type::Map(Box::new(K::duck_type()), Box::new(V::duck_type())) + } +} + +impl FromDuck for HashMap { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let mut path: Vec<_> = path_to_field.into(); + let mut out = HashMap::new(); + + match val { + Some(Value::Map(x)) => { + // TODO: do not clone + for (k, v) in x.iter() { + path.push(format!("KEY_{}", out.len())); + let k = K::from_duck_with_path(Some(k.clone()), &path)?; + path.pop(); + + path.push(format!("{k}")); + let v = V::from_duck_with_path(Some(v.clone()), &path)?; + path.pop(); + + out.insert(k, v); + } + } + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(out); + } +} + +impl ToDuck for HashMap { + #[inline] + fn to_duck_with_path(&self, path_to_field: &[String]) -> Result { + let mut path: Vec<_> = path_to_field.into(); + + let mut out = Vec::with_capacity(self.len()); + for (k, v) in self.iter() { + path.push(format!("KEY_{}", out.len())); + let kv = k.to_duck_with_path(&path)?; + path.pop(); + + path.push(format!("{k}")); + let vv = v.to_duck_with_path(&path)?; + path.pop(); + + out.push((kv, vv)) + } + + let out = OrderedMap::from(out); + return Ok(Value::Map(out)); + } +} diff --git a/crates/libduck/src/from_duck.rs b/crates/libduck/src/from_duck.rs new file mode 100644 index 0000000..4073d8f --- /dev/null +++ b/crates/libduck/src/from_duck.rs @@ -0,0 +1,159 @@ +use duckdb::types::{Type, Value}; +use itertools::Itertools; +use std::{ + error::Error, + fmt::{Debug, Display}, +}; + +use super::DuckValue; + +/// An error we can encounter when +/// converting a [`duckdb::types::Value`] to a struct. +pub enum FromDuckError { + /// We encountered a duckdb object with a type + /// that cannot be converted in the requested Rust object + InvalidType { + /// Where this field is (see [`FromDuck::from_duck_with_path`]) + path_to_field: Vec, + + /// The type we expected to find + /// (other types may be valid) + expected: Type, + + /// The type we got. + /// This is [`None`] if we weren't able to infer it. + got: Option, + }, + + /// We had to decode a string to convert a duckdb object to a Rust type, + /// but encountered an error while decoding that string. + /// + /// for example, this error will be returned when + /// a TEXT field contains an invalid [`url::Url`]. + ParseError { + /// Where this field is (see [`FromDuck::from_duck_with_path`]) + path_to_field: Vec, + + /// The string we tried to deserialize + string: String, + + /// A message with a quick overview of the error + msg: Option, + + /// The error we encountered + parent: Option>, + }, +} + +impl std::error::Error for FromDuckError { + fn cause(&self) -> Option<&dyn Error> { + match self { + Self::ParseError { parent, .. } => parent.as_ref().map(|v| &**v), + _ => None, + } + } +} + +impl Debug for FromDuckError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(self, f) + } +} + +impl Display for FromDuckError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidType { + path_to_field, + expected, + got, + } => match got { + None => { + write!( + f, + "unexpected type at {} (expected {expected})", + path_to_field.iter().join(".") + ) + } + Some(got) => { + write!( + f, + "unexpected type at {} (expected {expected}, got {got:?})", + path_to_field.iter().join(".") + ) + } + }, + + Self::ParseError { + path_to_field, + string, + msg: Some(msg), + parent: _parent, + } => { + write!( + f, + "could not parse string at {}: {msg} (while parsing {string:?})", + path_to_field.iter().join(".") + ) + } + + Self::ParseError { + path_to_field, + string, + msg: None, + parent: _parent, + } => { + write!( + f, + "could not parse string at {} (while parsing {string:?})", + path_to_field.iter().join(".") + ) + } + } + } +} + +// +// MARK: FromDuck +// + +/// Trait for types that can be decoded from DuckDB values. +/// This trait provides a type-safe way to convert [`duckdb::types::Value`] instances into Rust types. +/// It can be automatically derived using `#[derive(FromDuck)]`. +/// +/// [`FromDuck`] is very similar to [`duckdb::types::FromSql`], with the following differences +/// - [`duckdb::types::FromSql`] operates on references and arrow types, while [`FromDuck`] operates on owned values +/// - [`FromDuck`] provides much better error reporting +/// - [`FromDuck`] may be derived +pub trait FromDuck: Sized + DuckValue { + /// Try to decode this type from a duck value. \ + /// **In nearly all cases, you don't want to call this function.** \ + /// Call [`FromDuck::from_duck`] instead. + /// + /// ## `path_to_field` + /// `path_to_field` is an path to the field name we're decoding, + /// and allows us to generate helpful errors. + /// + /// For example, if we're decoding the field "url" in the following schema: + /// ```notrust + /// STRUCT ( + /// page STRUCT ( + /// url VARCHAR + /// ) + /// ) + /// ``` + /// + /// `path_to_field` will be `["page", "url"]` + /// + /// This array should be empty if you're decoding a "root" struct + /// (which will usually be the case when decoding rows returned by a query) + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result; + + /// Try to decode this type from a duck value. + fn from_duck(val: Value) -> Result { + return Self::from_duck_with_path(Some(val), &[]); + } +} diff --git a/crates/libduck/src/lib.rs b/crates/libduck/src/lib.rs new file mode 100644 index 0000000..46d514e --- /dev/null +++ b/crates/libduck/src/lib.rs @@ -0,0 +1,133 @@ +//! [`libduck`] provides quality-of-life improvements on top of [duckdb-rs](https://github.com/duckdb/duckdb-rs). +//! +//! This crate re-exports [`duckdb`]. This makes sure we only have one version of [`duckdb`] in the source tree. +//! +//! # Features +//! +//! - [`FromDuck`] allows conversion from a [`duckdb::types::Value`] to an arbitrary Rust struct +//! - [`ToDuck`] allows conversion from an arbitrary Rust struct to a [`duckdb::types::Value`] +//! - [`DuckValue`] is a dependency for both [`FromDuck`] and [`ToDuck`]. +//! +//! All the traits above can be derived see the `#[derive]` macro doc-comments for details. +//! +//! +//! # Caveats +//! +//! ## `Vec` is not a binary blob +//! When deriving [`FromDuck`] or [`ToDuck`], `Vec`s become `TINYINT[]`. \ +//! If you want a proper binary blob (which you almost certainly do), use [`DuckBlob`] instead. +//! +//! ## [`ToDuck`] panics +//! A few code paths in [duckdb-rs](https://github.com/duckdb/duckdb-rs) are not implemented, and panic when we try to call them. +//! This makes the [`ToDuck`] trait difficult to use directly. Structs and arrays are among the values that panic! +//! +//! This may be hacked around using `CAST(? AS JSON)`. See [`ToDuck`] for details. + +use duckdb::types::{Type, Value}; +use itertools::Itertools; + +pub use duckdb; +pub use libduck_derive::{DuckValue, FromDuck, ToDuck}; + +mod from_duck; +pub use from_duck::*; + +mod to_duck; +pub use to_duck::*; + +mod containers; +pub use containers::DuckBlob; +mod primitives; +pub mod special; + +/// Something that can be converted into a DuckDB value. +pub trait DuckValue { + /// Get the duckdb type this object deserializes from. + /// This is only used to produce helpful errors. + fn duck_type() -> Type; +} + +/// Try to infer a [`Value`]'s duckdb [`Type`]. +/// Returns [`None`] if the value's type cannot be inferred. +/// +/// This is similar to [`Value::data_type()`], and is only used to generate helpful errors. +/// +/// You should never need to use this function manually. +/// It would be private if it wasn't inside the code generated by `#[derive(FromDuck)]` +#[inline] +pub fn _infer_value_type(value: &Value) -> Option { + return Some(match value { + Value::Null => Type::Null, + Value::Boolean(_) => Type::Boolean, + Value::TinyInt(_) => Type::TinyInt, + Value::SmallInt(_) => Type::SmallInt, + Value::Int(_) => Type::Int, + Value::BigInt(_) => Type::BigInt, + Value::HugeInt(_) => Type::HugeInt, + Value::UTinyInt(_) => Type::UTinyInt, + Value::USmallInt(_) => Type::USmallInt, + Value::UInt(_) => Type::UInt, + Value::UBigInt(_) => Type::UBigInt, + Value::Float(_) => Type::Float, + Value::Double(_) => Type::Double, + Value::Decimal(_) => Type::Decimal, + Value::Timestamp(_, _) => Type::Timestamp, + Value::Text(_) => Type::Text, + Value::Blob(_) => Type::Blob, + Value::Date32(_) => Type::Date32, + Value::Time64(..) => Type::Time64, + Value::Interval { .. } => Type::Interval, + Value::Enum(..) => Type::Enum, + Value::Union(_) => Type::Union, + + Value::List(x) => { + // Edge case: x is null (also true for map) + if !x.iter().map(_infer_value_type).all_equal() { + return None; + }; + + let value = x.first()?; + let vt = _infer_value_type(value)?; + + Type::List(Box::new(vt)) + } + + Value::Map(x) => { + if !x.keys().map(_infer_value_type).all_equal() { + return None; + }; + + if !x.values().map(_infer_value_type).all_equal() { + return None; + }; + + let key = x.keys().next()?; + let kt = _infer_value_type(key)?; + + let value = x.values().next()?; + let vt = _infer_value_type(value)?; + + Type::Map(Box::new(kt), Box::new(vt)) + } + + Value::Struct(x) => { + let mut out = Vec::new(); + for (k, v) in x.iter() { + out.push((k.into(), _infer_value_type(v)?)); + } + + Type::Struct(out) + } + + Value::Array(x) => { + if !x.iter().map(_infer_value_type).all_equal() { + return None; + }; + + Type::Array( + Box::new(_infer_value_type(x.first()?)?), + x.len().try_into().ok()?, + ) + } + }); +} diff --git a/crates/libduck/src/primitives.rs b/crates/libduck/src/primitives.rs new file mode 100644 index 0000000..40e42a3 --- /dev/null +++ b/crates/libduck/src/primitives.rs @@ -0,0 +1,729 @@ +//! This module implements [`FromDuck`], [`ToDuck`], and [`DuckValue`] for all primitive types. + +use chrono::{DateTime, TimeZone, Utc}; +use duckdb::types::{TimeUnit, Type, Value}; +use url::Url; +use uuid::Uuid; + +use super::{ + from_duck::{FromDuck, FromDuckError}, + to_duck::{ToDuck, ToDuckError}, +}; +use crate::{DuckValue, _infer_value_type}; + +// MARK: DateTime + +impl DuckValue for DateTime { + #[inline] + fn duck_type() -> Type { + Type::Timestamp + } +} + +impl FromDuck for DateTime { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let timestamp = match val { + Some(Value::Timestamp(time_unit, time_value)) => { + let value = time_value; + match time_unit { + TimeUnit::Second => Utc.timestamp_opt(value, 0).unwrap(), + TimeUnit::Millisecond => Utc.timestamp_millis_opt(value).unwrap(), + TimeUnit::Microsecond => Utc.timestamp_micros(value).unwrap(), + TimeUnit::Nanosecond => Utc.timestamp_nanos(value), + } + } + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(timestamp); + } +} + +impl ToDuck for DateTime { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + let micros = self.timestamp_micros(); + Ok(Value::Timestamp(TimeUnit::Microsecond, micros)) + } +} + +// MARK: Uuid + +impl DuckValue for Uuid { + #[inline] + fn duck_type() -> Type { + Type::Text + } +} + +impl FromDuck for Uuid { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let uuid = match val { + Some(Value::Text(uuid_string)) => match uuid_string.parse() { + Ok(x) => x, + Err(e) => { + return Err(FromDuckError::ParseError { + path_to_field: path_to_field.into(), + string: uuid_string, + msg: Some("invalid uuid".into()), + parent: Some(Box::new(e)), + }); + } + }, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(uuid); + } +} + +impl ToDuck for Uuid { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Text(self.to_string())) + } +} + +// MARK: Url + +impl DuckValue for Url { + #[inline] + fn duck_type() -> Type { + Type::Text + } +} + +impl FromDuck for Url { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let url = match val { + Some(Value::Text(url_string)) => match url_string.parse() { + Ok(x) => x, + Err(e) => { + return Err(FromDuckError::ParseError { + path_to_field: path_to_field.into(), + string: url_string, + msg: Some("invalid url".into()), + parent: Some(Box::new(e)), + }); + } + }, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(url); + } +} + +impl ToDuck for Url { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Text(self.to_string())) + } +} + +// MARK: bool + +impl DuckValue for bool { + #[inline] + fn duck_type() -> Type { + Type::Boolean + } +} + +impl FromDuck for bool { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + match val { + Some(Value::Boolean(x)) => return Ok(x), + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + } +} + +impl ToDuck for bool { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Boolean(*self)) + } +} + +// MARK: String + +impl DuckValue for String { + #[inline] + fn duck_type() -> Type { + Type::Text + } +} + +impl FromDuck for String { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let string = match val { + Some(Value::Text(s)) => s, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(string); + } +} + +impl ToDuck for String { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Text(self.clone())) + } +} + +// MARK: f64 + +impl DuckValue for f64 { + #[inline] + fn duck_type() -> Type { + Type::Double + } +} + +impl FromDuck for f64 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::Double(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for f64 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Double(*self)) + } +} + +// MARK: f32 + +impl DuckValue for f32 { + #[inline] + fn duck_type() -> Type { + Type::Float + } +} + +impl FromDuck for f32 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::Float(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for f32 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Float(*self)) + } +} + +// MARK: i32 + +impl DuckValue for i32 { + #[inline] + fn duck_type() -> Type { + Type::Int + } +} + +impl FromDuck for i32 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::Int(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for i32 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::Int(*self)) + } +} + +// MARK: i64 + +impl DuckValue for i64 { + #[inline] + fn duck_type() -> Type { + Type::BigInt + } +} + +impl FromDuck for i64 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::BigInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for i64 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::BigInt(*self)) + } +} + +// MARK: i16 + +impl DuckValue for i16 { + #[inline] + fn duck_type() -> Type { + Type::SmallInt + } +} + +impl FromDuck for i16 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::SmallInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for i16 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::SmallInt(*self)) + } +} + +// MARK: i8 + +impl DuckValue for i8 { + #[inline] + fn duck_type() -> Type { + Type::TinyInt + } +} + +impl FromDuck for i8 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::TinyInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for i8 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::TinyInt(*self)) + } +} + +// MARK: u32 + +impl DuckValue for u32 { + #[inline] + fn duck_type() -> Type { + Type::UInt + } +} + +impl FromDuck for u32 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::UInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for u32 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::UInt(*self)) + } +} + +// MARK: u64 + +impl DuckValue for u64 { + #[inline] + fn duck_type() -> Type { + Type::UBigInt + } +} + +impl FromDuck for u64 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::UBigInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for u64 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::UBigInt(*self)) + } +} + +// MARK: u16 + +impl DuckValue for u16 { + #[inline] + fn duck_type() -> Type { + Type::USmallInt + } +} + +impl FromDuck for u16 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::USmallInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for u16 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::USmallInt(*self)) + } +} + +// MARK: u8 + +impl DuckValue for u8 { + #[inline] + fn duck_type() -> Type { + Type::UTinyInt + } +} + +impl FromDuck for u8 { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let v = match val { + Some(Value::UTinyInt(x)) => x, + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + return Ok(v); + } +} + +impl ToDuck for u8 { + #[inline] + fn to_duck_with_path(&self, _path_to_field: &[String]) -> Result { + Ok(Value::UTinyInt(*self)) + } +} diff --git a/crates/libduck/src/special.rs b/crates/libduck/src/special.rs new file mode 100644 index 0000000..8291384 --- /dev/null +++ b/crates/libduck/src/special.rs @@ -0,0 +1,87 @@ +//! This module provides a few utilities only used inside macros. +//! You shouldn't ever never use any code in here manually. + +use duckdb::types::{Type, Value}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; +use utoipa::ToSchema; + +use super::{ + from_duck::{FromDuck, FromDuckError}, + to_duck::{ToDuck, ToDuckError}, +}; +use crate::{DuckValue, _infer_value_type}; + +// MARK: DuckJson + +/// A JSON object stored as text in DuckDB with automatic deserialization. +#[derive(Debug, Clone, ToSchema)] +#[schema(value_type = T)] +pub struct DuckJson(pub T); + +impl DuckValue for DuckJson { + #[inline] + fn duck_type() -> Type { + Type::Text + } +} + +impl FromDuck for DuckJson { + #[inline] + fn from_duck_with_path( + val: Option, + path_to_field: &[String], + ) -> Result { + let string = match val { + Some(Value::Text(s)) => s.clone(), + + Some(x) => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: _infer_value_type(&x), + }); + } + + None => { + return Err(FromDuckError::InvalidType { + path_to_field: path_to_field.into(), + expected: Self::duck_type(), + got: None, + }); + } + }; + + let value: T = match serde_json::from_str(&string) { + Ok(x) => x, + Err(error) => { + return Err(FromDuckError::ParseError { + path_to_field: path_to_field.into(), + string, + msg: Some("could not deserialize json".into()), + parent: Some(Box::new(error)), + }); + } + }; + + return Ok(Self(value)); + } +} + +impl ToDuck for DuckJson { + #[inline] + fn to_duck_with_path(&self, path_to_field: &[String]) -> Result { + let string = match serde_json::to_string(&self.0) { + Ok(x) => x, + Err(error) => { + return Err(ToDuckError::EncodeError { + path_to_field: path_to_field.into(), + msg: Some("could not serialize json".into()), + parent: Some(Box::new(error)), + }); + } + }; + + return Ok(Value::Text(string)); + } +} diff --git a/crates/libduck/src/to_duck.rs b/crates/libduck/src/to_duck.rs new file mode 100644 index 0000000..d8adf53 --- /dev/null +++ b/crates/libduck/src/to_duck.rs @@ -0,0 +1,113 @@ +use duckdb::types::Value; +use itertools::Itertools; +use std::{ + error::Error, + fmt::{Debug, Display}, +}; + +use super::DuckValue; + +/// A error we can encounter when converting a Rust type to a [`duckdb::types::Value`]. +pub enum ToDuckError { + /// We encountered an error when encoding a Rust type as a string. + EncodeError { + /// Where this field is (see [`FromDuck::from_duck_with_path`]) + path_to_field: Vec, + + /// A message with a quick overview of the error + msg: Option, + + /// The error we encountered + parent: Option>, + }, +} + +impl std::error::Error for ToDuckError { + fn cause(&self) -> Option<&dyn Error> { + match self { + Self::EncodeError { parent, .. } => parent.as_ref().map(|v| &**v), + } + } +} + +impl Debug for ToDuckError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(self, f) + } +} + +impl Display for ToDuckError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodeError { + path_to_field, + msg: Some(msg), + parent: _parent, + } => { + write!( + f, + "could not parse string at {}: {msg}", + path_to_field.iter().join(".") + ) + } + + Self::EncodeError { + path_to_field, + msg: None, + parent: _parent, + } => { + write!( + f, + "could not encode string at {}", + path_to_field.iter().join(".") + ) + } + } + } +} + +/// # WARNING +/// Many parts of the duck sdk are not implemented, and will panic when we try to use them. +/// They may be inserted by serializing them to a JSON string and decoding with `CAST(? AS JSON)` in duckdb. +/// +/// This does not affect [`crate::FromDuck`], and only interferes when calling [`ToDuck::to_duck`]. +/// +/// +/// # Docs +/// Trait for types that can be converted to DuckDB values. +/// This trait provides a type-safe way to convert Rust types to [`duckdb::types::Value`]s. +/// It can be automatically derived using `#[derive(FromDuck)]`. +/// +/// [`ToDuck`] is very similar to [`duckdb::types::FromSql`], with the following differences +/// - [`duckdb::types::FromSql`] operates on references and arrow types, while [`ToDuck`] operates on owned values +/// - [`ToDuck`] provides much better error reporting +/// - [`ToDuck`] may be derived +pub trait ToDuck: Sized + DuckValue { + /// Try to encode this type as a duck value. \ + /// **In nearly all cases, you don't want to call this function.** \ + /// Call [`ToDuck::to_duck`] instead. + /// + /// ## `path_to_field` + /// `path_to_field` is an path to the field name we're decoding, + /// and allows us to generate helpful errors. + /// + /// For example, if we're decoding the field "url" in the following schema: + /// ```notrust + /// STRUCT ( + /// page STRUCT ( + /// url VARCHAR + /// ) + /// ) + /// ``` + /// + /// `path_to_field` will be `["page", "url"]` + /// + /// This array should be empty if you're decoding a "root" struct + /// (which will usually be the case when decoding rows returned by a query) + fn to_duck_with_path(&self, path_to_field: &[String]) -> Result; + + /// Try to encode this type as a duck value. + fn to_duck(&self) -> Result { + return self.to_duck_with_path(&[]); + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true