From b8d4110c22858f922cfe5f1db0171e301b536037 Mon Sep 17 00:00:00 2001 From: John Bledsoe <59550+johnbcodes@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:56:26 -0400 Subject: [PATCH] feat: Switch to Diesel for db layer.. Ulid for sortable ID generation.. fix: Fix line_items.line_item_date_id data type --- .env | 2 +- Cargo.lock | 307 ++++++++++-------- Cargo.toml | 11 +- Makefile | 35 ++ diesel.toml | 10 + migrations/.keep | 0 migrations/2023-10-31-123932_quotes/down.sql | 1 + migrations/2023-10-31-123932_quotes/up.sql | 13 + .../down.sql | 1 + .../up.sql} | 2 +- .../2023-10-31-123940_line_items/down.sql | 1 + .../up.sql} | 4 +- migrations/20230408081854_quotes.down.sql | 1 - migrations/20230408081854_quotes.up.sql | 13 - .../20230413082629_line_item_dates.down.sql | 1 - migrations/20230413082726_line_items.down.sql | 1 - src/currency.rs | 23 -- src/line_item_dates/controller.rs | 16 +- src/line_item_dates/model.rs | 12 +- src/line_item_dates/query.rs | 169 ++++------ src/line_items/controller.rs | 15 +- src/line_items/model.rs | 12 +- src/line_items/query.rs | 239 +++++--------- src/main.rs | 35 +- src/migrations.rs | 21 -- src/quotes/controller.rs | 16 +- src/quotes/model.rs | 36 +- src/quotes/query.rs | 156 ++++----- src/schema.rs | 41 +++ src/schema.rs.patch | 57 ++++ src/schema.rs.unpatched | 42 +++ src/time.rs | 54 +-- 32 files changed, 669 insertions(+), 678 deletions(-) create mode 100644 Makefile create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/2023-10-31-123932_quotes/down.sql create mode 100644 migrations/2023-10-31-123932_quotes/up.sql create mode 100644 migrations/2023-10-31-123938_line_item_dates/down.sql rename migrations/{20230413082629_line_item_dates.up.sql => 2023-10-31-123938_line_item_dates/up.sql} (86%) create mode 100644 migrations/2023-10-31-123940_line_items/down.sql rename migrations/{20230413082726_line_items.up.sql => 2023-10-31-123940_line_items/up.sql} (89%) delete mode 100644 migrations/20230408081854_quotes.down.sql delete mode 100644 migrations/20230408081854_quotes.up.sql delete mode 100644 migrations/20230413082629_line_item_dates.down.sql delete mode 100644 migrations/20230413082726_line_items.down.sql delete mode 100644 src/migrations.rs create mode 100644 src/schema.rs create mode 100644 src/schema.rs.patch create mode 100644 src/schema.rs.unpatched diff --git a/.env b/.env index 9359f40..ec5f23e 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_FILE=data/demo.db +DATABASE_URL=data/demo.db diff --git a/Cargo.lock b/Cargo.lock index 94824cd..21e631f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -52,12 +41,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "anyhow" version = "1.0.75" @@ -154,19 +137,17 @@ dependencies = [ "axum", "convert_case", "currency_rs", + "diesel", + "diesel_migrations", "dotenvy", "hotwire-turbo", "hotwire-turbo-axum", "itertools", + "libsqlite3-sys", "markup", "mime_guess", - "nanoid", "once_cell", - "r2d2", - "r2d2_sqlite", "regex", - "rusqlite", - "rusqlite_migration", "rust-embed", "serde", "serde_json", @@ -175,6 +156,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "ulid", "validator", ] @@ -195,9 +177,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bit-set" @@ -248,9 +230,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -289,9 +271,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -318,9 +300,9 @@ dependencies = [ [[package]] name = "currency_rs" version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee50374562062a2b38294fd4ac424acba5c587cc9b361b2dfcf3b8bb6f32a" +source = "git+https://github.com/johnbcodes/currency_rs?branch=feature/db-diesel2-sqlite#74cbbca4d98ec9dbf6faceb4a4a18a6fcdb84469" dependencies = [ + "diesel", "fancy-regex", "lazy_static", ] @@ -335,6 +317,50 @@ dependencies = [ "serde", ] +[[package]] +name = "diesel" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2268a214a6f118fce1838edba3d1561cf0e78d8de785475957a580a7f8c69d33" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.38", +] + [[package]] name = "digest" version = "0.10.7" @@ -378,16 +404,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] -name = "fallible-iterator" -version = "0.2.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "fancy-regex" @@ -426,36 +446,36 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-task", @@ -496,19 +516,6 @@ name = "hashbrown" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown", -] [[package]] name = "hermit-abi" @@ -599,7 +606,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -622,6 +629,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "iri-string" version = "0.7.0" @@ -737,6 +754,27 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -764,24 +802,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", "windows-sys", ] -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -955,17 +984,6 @@ dependencies = [ "scheduled-thread-pool", ] -[[package]] -name = "r2d2_sqlite" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f31323d6161385f385046738df520e0e8694fa74852d35891fc0be08348ddc" -dependencies = [ - "r2d2", - "rusqlite", - "uuid", -] - [[package]] name = "rand" version = "0.8.5" @@ -1069,31 +1087,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rusqlite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" -dependencies = [ - "bitflags 2.4.1", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "serde_json", - "smallvec", -] - -[[package]] -name = "rusqlite_migration" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef7dd29a4426624704d5966416682fb7ab3682f724986e9e3893eaca44accabc" -dependencies = [ - "log", - "rusqlite", -] - [[package]] name = "rust-embed" version = "8.0.0" @@ -1173,18 +1166,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -1193,9 +1186,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1212,6 +1205,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1279,9 +1281,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -1289,9 +1291,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys", @@ -1413,7 +1415,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] @@ -1431,9 +1433,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -1442,6 +1444,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1535,12 +1571,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -1574,6 +1610,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ulid" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e37c4b6cbcc59a8dcd09a6429fbc7890286bcbb79215cea7b38a3c4c0921d93" +dependencies = [ + "rand", +] + [[package]] name = "unicase" version = "2.7.0" @@ -1634,7 +1679,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", - "rand", ] [[package]] @@ -1819,6 +1863,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +dependencies = [ + "memchr", +] + [[package]] name = "zstd" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index dfa626f..e07746b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,20 +16,18 @@ lto = true anyhow = "1.0" axum = "0.6" convert_case = "0.6" -currency_rs = "1.1" +currency_rs = { git = "https://github.com/johnbcodes/currency_rs", branch = "feature/db-diesel2-sqlite", version = "1.1", features = [ "db-diesel2-sqlite" ] } +diesel = { version = "2.1", features = ["r2d2", "sqlite", "time"] } +diesel_migrations = "2.1" dotenvy = "0.15" hotwire-turbo = "0.1" hotwire-turbo-axum = "0.1" itertools = "0.11" +libsqlite3-sys = { version = "0.26", features = ["bundled"] } markup = "0.13" mime_guess = "2" -nanoid = "0.4" once_cell = "1" -r2d2 = "0.8" -r2d2_sqlite = "0.22" regex = "1" -rusqlite = { version = "0.29", features = ["bundled", "serde_json"] } -rusqlite_migration = "1" rust-embed = { version = "8", features = ["interpolate-folder-path"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -38,4 +36,5 @@ tokio = { version = "1", features = ["full"] } tower-http = { version = "0.4", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +ulid = "1.1" validator = { version = "0.16", features = ["derive"] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..299a110 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# Usage: +# +# Do any modifications to `diesel.toml` and/or run migrations and then re-make the schema: +# +# $ make src/schema.rs +# +# Do any modifications to `src/schema.rs` and then re-make the patch: +# +# $ make src/schema.rs.patch + +SHELL = /bin/bash + +.PHONY: src/schema.rs +src/schema.rs: + # patch file must exist if it's defined in diesel.toml + touch src/schema.rs.patch + # print the schema + diesel print-schema > src/schema.rs + # make unpached version + make src/schema.rs.unpatched + # make the new patch file + make src/schema.rs.patch + +.PHONY: src/schema.rs.patch +src/schema.rs.patch: + diff -U6 src/schema.rs.unpatched src/schema.rs > src/schema.rs.patch || true + +.PHONY: src/schema.rs.unpatched +src/schema.rs.unpatched: + # if patch isn't empty, create schema.rs.unpatched from the existing patch + [ ! -s src/schema.rs.patch ] || \ + patch -p0 -R -o src/schema.rs.unpatched < src/schema.rs.patch + # otherwise create schema.rs.unpatched by copying schema.rs + [ -f src/schema.rs.unpatched ] || \ + cp -a src/schema.rs src/schema.rs.unpatched \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..79c3eb6 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,10 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] +patch_file = "src/schema.rs.patch" + +[migrations_directory] +dir = "migrations" diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2023-10-31-123932_quotes/down.sql b/migrations/2023-10-31-123932_quotes/down.sql new file mode 100644 index 0000000..e2dd0ce --- /dev/null +++ b/migrations/2023-10-31-123932_quotes/down.sql @@ -0,0 +1 @@ +drop table quotes; diff --git a/migrations/2023-10-31-123932_quotes/up.sql b/migrations/2023-10-31-123932_quotes/up.sql new file mode 100644 index 0000000..a1c0420 --- /dev/null +++ b/migrations/2023-10-31-123932_quotes/up.sql @@ -0,0 +1,13 @@ +create table quotes ( + id text not null primary key, + name text not null, + created_at text not null, + updated_at text not null +); + +insert into quotes + (id, name, created_at, updated_at) +values + ('01HE2X4FKPDTVHHB6C2HZD5Z53','First quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')), + ('01HE2X51WKBYSJ7ZPETRB9STCQ','Second quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')), + ('01HE2X5BB7JNHNXP17H9HM7H9C','Third quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')); \ No newline at end of file diff --git a/migrations/2023-10-31-123938_line_item_dates/down.sql b/migrations/2023-10-31-123938_line_item_dates/down.sql new file mode 100644 index 0000000..1b6bb4f --- /dev/null +++ b/migrations/2023-10-31-123938_line_item_dates/down.sql @@ -0,0 +1 @@ +drop table line_item_dates; diff --git a/migrations/20230413082629_line_item_dates.up.sql b/migrations/2023-10-31-123938_line_item_dates/up.sql similarity index 86% rename from migrations/20230413082629_line_item_dates.up.sql rename to migrations/2023-10-31-123938_line_item_dates/up.sql index 305ca87..00f83bf 100644 --- a/migrations/20230413082629_line_item_dates.up.sql +++ b/migrations/2023-10-31-123938_line_item_dates/up.sql @@ -9,4 +9,4 @@ create table line_item_dates ( create unique index idx_quote_id_and_date on line_item_dates (quote_id, "date"); create index idx_date on line_item_dates ("date"); -create index idx_quote_id on line_item_dates (quote_id); \ No newline at end of file +create index idx_quote_id on line_item_dates (quote_id); diff --git a/migrations/2023-10-31-123940_line_items/down.sql b/migrations/2023-10-31-123940_line_items/down.sql new file mode 100644 index 0000000..d3e3ff7 --- /dev/null +++ b/migrations/2023-10-31-123940_line_items/down.sql @@ -0,0 +1 @@ +drop table line_items; diff --git a/migrations/20230413082726_line_items.up.sql b/migrations/2023-10-31-123940_line_items/up.sql similarity index 89% rename from migrations/20230413082726_line_items.up.sql rename to migrations/2023-10-31-123940_line_items/up.sql index 0d7919f..f962e90 100644 --- a/migrations/20230413082726_line_items.up.sql +++ b/migrations/2023-10-31-123940_line_items/up.sql @@ -1,6 +1,6 @@ create table line_items ( id text not null primary key, - line_item_date_id integer not null, + line_item_date_id text not null, name text not null, description text, quantity integer not null, @@ -10,4 +10,4 @@ create table line_items ( foreign key(line_item_date_id) references line_item_dates(id) ); -create index idx_line_item_date_id on line_items (line_item_date_id); \ No newline at end of file +create index idx_line_item_date_id on line_items (line_item_date_id); diff --git a/migrations/20230408081854_quotes.down.sql b/migrations/20230408081854_quotes.down.sql deleted file mode 100644 index 76b1dc7..0000000 --- a/migrations/20230408081854_quotes.down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table quotes; \ No newline at end of file diff --git a/migrations/20230408081854_quotes.up.sql b/migrations/20230408081854_quotes.up.sql deleted file mode 100644 index b89596c..0000000 --- a/migrations/20230408081854_quotes.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table quotes ( - id text not null primary key, - name text not null, - created_at text not null, - updated_at text not null -); - -insert into quotes - (id, name, created_at, updated_at) -values - ('HLB99rYWEnVeZmzTpACXW','First quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')), - ('2iCNgm7s44hn-V4ofYbMY','Second quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')), - ('ez-f61kn7U5-YqpoGAdvU','Third quote',strftime('%Y-%m-%dT%H:%M:%fZ'),strftime('%Y-%m-%dT%H:%M:%fZ')); \ No newline at end of file diff --git a/migrations/20230413082629_line_item_dates.down.sql b/migrations/20230413082629_line_item_dates.down.sql deleted file mode 100644 index adbca82..0000000 --- a/migrations/20230413082629_line_item_dates.down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table line_item_dates; \ No newline at end of file diff --git a/migrations/20230413082726_line_items.down.sql b/migrations/20230413082726_line_items.down.sql deleted file mode 100644 index d16b2ed..0000000 --- a/migrations/20230413082726_line_items.down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table line_items; \ No newline at end of file diff --git a/src/currency.rs b/src/currency.rs index e9fc195..a2fcc61 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -1,28 +1,5 @@ -use currency_rs::Currency; use once_cell::sync::Lazy; use regex::Regex; -use rusqlite::{ - types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}, - Result, -}; pub(crate) static FORM_CURRENCY_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\d*(\.\d{2})?$").unwrap()); - -pub(crate) struct CurrencySql(pub(crate) Currency); - -impl FromSql for CurrencySql { - fn column_result(value: ValueRef) -> FromSqlResult { - match value { - ValueRef::Integer(x) => Ok(CurrencySql(Currency::new_float(x as f64, None))), - ValueRef::Real(x) => Ok(CurrencySql(Currency::new_float(x, None))), - _ => Err(FromSqlError::InvalidType), - } - } -} - -impl ToSql for CurrencySql { - fn to_sql(&self) -> Result { - Ok(ToSqlOutput::from(self.0.value())) - } -} diff --git a/src/line_item_dates/controller.rs b/src/line_item_dates/controller.rs index e7b0094..3b7a68c 100644 --- a/src/line_item_dates/controller.rs +++ b/src/line_item_dates/controller.rs @@ -12,22 +12,22 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse}, }; +use diesel::prelude::SqliteConnection; +use diesel::r2d2::{ConnectionManager, Pool}; use hotwire_turbo_axum::TurboStream; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; use std::time::Instant; use tracing::info; use validator::Validate; pub(crate) async fn new( - State(pool): State>, + State(pool): State>>, Path(quote_id): Path, ) -> Result { let start = Instant::now(); let quote = quotes::query::read(&pool, quote_id).await?; let duration = start.elapsed().as_micros(); info!("quo - read duration: {duration} μs"); - let line_item_date = LineItemDatePresenter::from_quote(quote); + let line_item_date = LineItemDatePresenter::from_quote_with_total(quote); let duration = start.elapsed().as_micros(); info!("lid - read duration: {duration} μs"); Ok(Html( @@ -42,7 +42,7 @@ pub(crate) async fn new( } pub(crate) async fn create( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -84,7 +84,7 @@ pub(crate) async fn create( } pub(crate) async fn edit( - State(pool): State>, + State(pool): State>>, Path(id): Path, ) -> Result { let start = Instant::now(); @@ -104,7 +104,7 @@ pub(crate) async fn edit( } pub(crate) async fn update( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -154,7 +154,7 @@ pub(crate) async fn update( } pub(crate) async fn delete( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let start = Instant::now(); diff --git a/src/line_item_dates/model.rs b/src/line_item_dates/model.rs index 0a50d20..ddcf5e7 100644 --- a/src/line_item_dates/model.rs +++ b/src/line_item_dates/model.rs @@ -1,13 +1,15 @@ use crate::{ - quotes::model::Quote, + quotes::model::QuoteWithTotal, + schema::line_item_dates, time::{long_form, parse_date, short_form, DATE_REGEX}, }; -use nanoid::nanoid; +use diesel::prelude::*; use serde::Deserialize; use time::{Date, OffsetDateTime}; +use ulid::Ulid; use validator::Validate; -#[derive(Debug)] +#[derive(Debug, Insertable, Queryable)] pub struct LineItemDate { pub id: String, pub quote_id: String, @@ -20,7 +22,7 @@ impl From<&LineItemDateForm> for LineItemDate { fn from(value: &LineItemDateForm) -> Self { let date = parse_date(&value.date); LineItemDate { - id: value.id.clone().unwrap_or(nanoid!()), + id: value.id.clone().unwrap_or(Ulid::new().to_string()), quote_id: value.quote_id.clone(), date, created_at: OffsetDateTime::now_utc(), @@ -47,7 +49,7 @@ pub struct LineItemDatePresenter { } impl LineItemDatePresenter { - pub fn from_quote(quote: Quote) -> LineItemDatePresenter { + pub fn from_quote_with_total(quote: QuoteWithTotal) -> LineItemDatePresenter { LineItemDatePresenter { quote_id: quote.id, ..Default::default() diff --git a/src/line_item_dates/query.rs b/src/line_item_dates/query.rs index 1dca840..e8ae1e2 100644 --- a/src/line_item_dates/query.rs +++ b/src/line_item_dates/query.rs @@ -1,159 +1,102 @@ use crate::{ line_item_dates::model::{LineItemDate, LineItemDateForm}, line_items, - time::{DateSql, DateTimeSql}, + schema::line_item_dates, Result, }; -use r2d2::{Pool, PooledConnection}; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{Row, Transaction}; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; pub(crate) async fn all>( - pool: &Pool, - quote_id: S, + pool: &Pool>, + id: S, ) -> Result> { - // language=SQL - let sql = r#" - select - id, - quote_id, - date, - created_at, - updated_at - from line_item_dates - where quote_id = ? - order by date - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - let records = statement - .query_map(["e_id.as_ref()], map_result) - .unwrap() - .map(|result| result.unwrap()) - .collect(); + let mut connection = pool.get()?; + let records = line_item_dates::table + .filter(line_item_dates::quote_id.eq(&id.as_ref())) + .get_results(&mut connection)?; Ok(records) } pub(crate) async fn read>( - pool: &Pool, + pool: &Pool>, id: S, ) -> Result { - let connection = pool.get()?; - read_from_connection(&connection, id) + let mut connection = pool.get()?; + read_from_connection(&mut connection, id) } fn read_from_connection>( - connection: &PooledConnection, + connection: &mut PooledConnection>, id: S, ) -> Result { - // language=SQL - let sql = r#" - select - id, - quote_id, - date, - created_at, - updated_at - from line_item_dates - where id = ? - "#; - let mut statement = connection.prepare_cached(sql)?; - let record = statement.query_row([id.as_ref()], map_result)?; + let record = line_item_dates::table + .filter(line_item_dates::id.eq(&id.as_ref())) + .get_result(connection)?; Ok(record) } pub(crate) async fn insert( - pool: &Pool, + pool: &Pool>, form: &LineItemDateForm, ) -> Result { let record: LineItemDate = form.into(); - // language=SQL - let sql = r#" - insert into line_item_dates - (id, quote_id, "date", created_at, updated_at) - values - (?, ?, ?, ?, ?); - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute(( - &record.id, - &record.quote_id, - &DateSql(record.date), - &DateTimeSql(record.created_at), - &DateTimeSql(record.updated_at), - ))?; + + let mut connection = pool.get()?; + diesel::dsl::insert_into(line_item_dates::table) + .values(&record) + .execute(&mut connection)?; Ok(record) } pub(crate) async fn update( - pool: &Pool, + pool: &Pool>, form: &LineItemDateForm, ) -> Result { let record: LineItemDate = form.into(); - // language=SQL - let sql = r#" - update line_item_dates - set "date" = ?, - updated_at = ? - where id = ?; - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute(( - &DateSql(record.date), - &DateTimeSql(record.updated_at), - &record.id, - ))?; - read_from_connection(&connection, &record.id) + + let mut connection = pool.get()?; + diesel::dsl::update(line_item_dates::table) + .set(( + line_item_dates::date.eq(&record.date), + line_item_dates::updated_at.eq(&record.updated_at), + )) + .filter(line_item_dates::id.eq(&record.id)) + .execute(&mut connection)?; + + read_from_connection(&mut connection, &record.id) } pub(crate) async fn delete>( - pool: &Pool, + pool: &Pool>, id: S, ) -> Result { - let record = read(pool, &id).await?; let mut connection = pool.get()?; - let mut tx = connection.transaction()?; - line_items::query::delete_all_for_date(&mut tx, &id)?; - delete_line_item_date(&mut tx, &id)?; - tx.commit()?; - Ok(record) -} -fn delete_line_item_date>(tx: &mut Transaction<'_>, id: S) -> Result { - // language=SQL - let sql = r#"delete from line_item_dates where id = ?"#; - let mut statement = tx.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; - Ok(()) + let record = read_from_connection(&mut connection, &id)?; + _ = connection.transaction::<_, _, _>(|tx| { + line_items::query::delete_all_for_date(tx, &id)?; + + _ = diesel::dsl::delete(line_item_dates::table) + .filter(line_item_dates::id.eq(&id.as_ref())) + .execute(tx)?; + + Ok::<(), crate::error::AppError>(()) + }); + + Ok(record) } -pub(crate) fn delete_all_for_quote>(tx: &mut Transaction<'_>, id: S) -> Result { - // language=SQL - let sql = r#" - delete from line_item_dates - where id in ( - select - id - from line_item_dates - where quote_id = ? - ); - "#; +pub(crate) fn delete_all_for_quote>( + tx: &mut PooledConnection>, + id: S, +) -> Result { line_items::query::delete_all_for_quote(tx, &id)?; - let mut statement = tx.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; - Ok(()) -} -#[inline] -fn map_result(row: &Row<'_>) -> rusqlite::Result { - Ok(LineItemDate { - id: row.get(0)?, - quote_id: row.get(1)?, - date: row.get::<_, DateSql>(2)?.0, - created_at: row.get::<_, DateTimeSql>(3)?.0, - updated_at: row.get::<_, DateTimeSql>(4)?.0, - }) + _ = diesel::dsl::delete(line_item_dates::table) + .filter(line_item_dates::quote_id.eq(&id.as_ref())) + .execute(tx)?; + + Ok(()) } diff --git a/src/line_items/controller.rs b/src/line_items/controller.rs index e484714..85d6190 100644 --- a/src/line_items/controller.rs +++ b/src/line_items/controller.rs @@ -12,15 +12,15 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse}, }; +use diesel::prelude::SqliteConnection; +use diesel::r2d2::{ConnectionManager, Pool}; use hotwire_turbo_axum::TurboStream; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; use std::time::Instant; use tracing::info; use validator::Validate; pub(crate) async fn new( - State(pool): State>, + State(pool): State>>, Path(line_item_date_id): Path, ) -> Result { let start = Instant::now(); @@ -39,7 +39,7 @@ pub(crate) async fn new( } pub(crate) async fn create( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -88,7 +88,7 @@ pub(crate) async fn create( } pub(crate) async fn edit( - State(pool): State>, + State(pool): State>>, Path(id): Path, ) -> Result { let start = Instant::now(); @@ -115,7 +115,7 @@ pub(crate) async fn edit( } pub(crate) async fn update( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -128,6 +128,7 @@ pub(crate) async fn update( let start = Instant::now(); let quote = quotes::query::from_line_item_date_id(&pool, &form.line_item_date_id).await?; + info!("Quote total after update: {}", quote.total); let duration = start.elapsed().as_micros(); info!("quo - read duration: {duration} μs"); Ok(TurboStream( @@ -166,7 +167,7 @@ pub(crate) async fn update( } pub(crate) async fn delete( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let start = Instant::now(); diff --git a/src/line_items/model.rs b/src/line_items/model.rs index 98862d0..289554b 100644 --- a/src/line_items/model.rs +++ b/src/line_items/model.rs @@ -1,17 +1,19 @@ use crate::currency::FORM_CURRENCY_REGEX; +use crate::schema::line_items; use currency_rs::Currency; -use nanoid::nanoid; +use diesel::prelude::*; use serde::Deserialize; use time::OffsetDateTime; +use ulid::Ulid; use validator::Validate; -#[derive(Debug)] +#[derive(Debug, Insertable, Queryable, Selectable)] pub(crate) struct LineItem { pub(crate) id: String, pub(crate) line_item_date_id: String, pub(crate) name: String, pub(crate) description: Option, - pub(crate) quantity: u32, + pub(crate) quantity: i32, pub(crate) unit_price: Currency, pub(crate) created_at: OffsetDateTime, pub(crate) updated_at: OffsetDateTime, @@ -26,7 +28,7 @@ impl From<&LineItemForm> for LineItem { Some(description) }; LineItem { - id: value.id.clone().unwrap_or(nanoid!()), + id: value.id.clone().unwrap_or(Ulid::new().to_string()), line_item_date_id: value.line_item_date_id.clone(), name: value.name.clone(), description, @@ -50,7 +52,7 @@ pub(crate) struct LineItemForm { #[validate(length(min = 1, message = "can't be blank"))] pub(crate) name: String, pub(crate) description: Option, - pub(crate) quantity: u32, + pub(crate) quantity: i32, #[validate(regex(path = "FORM_CURRENCY_REGEX"))] pub(crate) unit_price: String, } diff --git a/src/line_items/query.rs b/src/line_items/query.rs index 0783291..04f2d5f 100644 --- a/src/line_items/query.rs +++ b/src/line_items/query.rs @@ -1,217 +1,130 @@ use crate::{ - currency::CurrencySql, line_items::model::{LineItem, LineItemForm}, - time::DateTimeSql, + schema::{line_item_dates, line_items}, Result, }; -use r2d2::{Pool, PooledConnection}; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{Row, Transaction}; -use tracing::info; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; pub(crate) async fn all_for_quote>( - pool: &Pool, + pool: &Pool>, quote_id: S, ) -> Result> { - // language=SQL - let sql = r#" - select - li.id, - li.line_item_date_id, - li.name, - li.description, - li.quantity, - li.unit_price, - li.created_at, - li.updated_at - from line_items li - inner join line_item_dates lid on lid.id = li.line_item_date_id - where lid.quote_id = ? - order by li.rowid - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - let records = statement - .query_map(["e_id.as_ref()], map_result) - .unwrap() - .map(|result| result.unwrap()) - .collect(); + let mut connection = pool.get()?; + let records = line_items::table + .inner_join(line_item_dates::table) + .select(LineItem::as_select()) + .filter(line_item_dates::quote_id.eq("e_id.as_ref())) + .get_results(&mut connection)?; + Ok(records) } pub(crate) async fn all_for_line_item_date>( - pool: &Pool, + pool: &Pool>, line_item_date_id: S, ) -> Result> { - // language=SQL - let sql = r#" - select - id, - line_item_date_id, - name, - description, - quantity, - unit_price, - created_at, - updated_at - from line_items - where line_item_date_id = ? - order by rowid - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - let records = statement - .query_map([&line_item_date_id.as_ref()], map_result) - .unwrap() - .map(|result| result.unwrap()) - .collect(); + let mut connection = pool.get()?; + let records = line_items::table + .filter(line_items::line_item_date_id.eq(&line_item_date_id.as_ref())) + .get_results(&mut connection)?; Ok(records) } pub(crate) async fn read>( - pool: &Pool, + pool: &Pool>, id: S, ) -> Result { - let connection = pool.get()?; - read_from_connection(&connection, id) + let mut connection = pool.get()?; + read_from_connection(&mut connection, id) } fn read_from_connection>( - connection: &PooledConnection, + connection: &mut PooledConnection>, id: S, ) -> Result { - // language=SQL - let sql = r#" - select - id, - line_item_date_id, - name, - description, - quantity, - unit_price, - created_at, - updated_at - from line_items - where id = ? - "#; - let mut statement = connection.prepare_cached(sql)?; - let record = statement.query_row([id.as_ref()], map_result)?; + let record = line_items::table + .filter(line_items::id.eq(&id.as_ref())) + .get_result(connection)?; Ok(record) } pub(crate) async fn insert( - pool: &Pool, + pool: &Pool>, form: &LineItemForm, ) -> Result { let record: LineItem = form.into(); - // language=SQL - let sql = r#" - insert into line_items - (id, line_item_date_id, name, description, quantity, unit_price, created_at, updated_at) - values - (?, ?, ?, ?, ?, ?, ?, ?); - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute(( - &record.id, - &record.line_item_date_id, - &record.name, - &record.description, - &record.quantity, - &CurrencySql(record.unit_price.clone()), - &DateTimeSql(record.created_at), - &DateTimeSql(record.updated_at), - ))?; + let mut connection = pool.get()?; + diesel::dsl::insert_into(line_items::table) + .values(&record) + .execute(&mut connection)?; Ok(record) } pub(crate) async fn update( - pool: &Pool, + pool: &Pool>, form: &LineItemForm, ) -> Result { let record: LineItem = form.into(); - info!("LineItem:\n{:?}", &record); - - // language=SQL - let sql = r#" - update line_items - set name = ?, - description = ?, - quantity = ?, - unit_price = ?, - updated_at = ? - where id = ?; - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute(( - &record.name, - &record.description, - &record.quantity, - &CurrencySql(record.unit_price), - &DateTimeSql(record.updated_at), - &record.id, - ))?; - read_from_connection(&connection, &record.id) + + let mut connection = pool.get()?; + diesel::dsl::update(line_items::table) + .set(( + line_items::name.eq(&record.name), + line_items::description.eq(&record.description), + line_items::quantity.eq(&record.quantity), + line_items::unit_price.eq(&record.unit_price), + line_items::updated_at.eq(&record.updated_at), + )) + .filter(line_items::id.eq(&record.id)) + .execute(&mut connection)?; + + read_from_connection(&mut connection, &record.id) } pub(crate) async fn delete>( - pool: &Pool, + pool: &Pool>, id: S, ) -> Result { - let record = read(pool, &id).await?; - // language=SQL - let sql = r#"delete from line_items where id = ?"#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; + let mut connection = pool.get()?; + let record = read_from_connection(&mut connection, &id)?; + + _ = diesel::dsl::delete(line_items::table) + .filter(line_items::id.eq(&id.as_ref())) + .execute(&mut connection)?; + Ok(record) } -pub(crate) fn delete_all_for_quote>(tx: &mut Transaction<'_>, id: S) -> Result { - // language=SQL - let sql = r#" - delete from line_items - where id in ( - select - li.id - from line_items li - inner join line_item_dates lid on lid.id = li.line_item_date_id - where lid.quote_id = ? - );"#; - let mut statement = tx.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; - Ok(()) -} +pub(crate) fn delete_all_for_quote>( + tx: &mut PooledConnection>, + quote_id: S, +) -> Result { + let line_items1 = diesel::alias!(line_items as line_items1); + + _ = diesel::dsl::delete(line_items::table) + .filter( + line_items::id.eq_any( + line_items1 + .inner_join(line_item_dates::table) + .select(line_items1.field(line_items::id)) + .filter(line_item_dates::quote_id.eq("e_id.as_ref())), + ), + ) + .execute(tx)?; -pub(crate) fn delete_all_for_date>(tx: &mut Transaction<'_>, id: S) -> Result { - // language=SQL - let sql = r#" - delete from line_items - where id in ( - select - id - from line_items - where line_item_date_id = ? - );"#; - let mut statement = tx.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; Ok(()) } -#[inline] -fn map_result(row: &Row<'_>) -> rusqlite::Result { - Ok(LineItem { - id: row.get(0)?, - line_item_date_id: row.get(1)?, - name: row.get(2)?, - description: row.get(3)?, - quantity: row.get(4)?, - unit_price: row.get::<_, CurrencySql>(5)?.0, - created_at: row.get::<_, DateTimeSql>(6)?.0, - updated_at: row.get::<_, DateTimeSql>(7)?.0, - }) +pub(crate) fn delete_all_for_date>( + tx: &mut PooledConnection>, + id: S, +) -> Result { + _ = diesel::dsl::delete(line_items::table) + .filter(line_items::line_item_date_id.eq(&id.as_ref())) + .execute(tx)?; + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 3f9fcf5..1157b55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,8 @@ mod error; mod layout; pub mod line_item_dates; pub mod line_items; -mod migrations; pub mod quotes; +mod schema; mod time; use assets::asset_handler; @@ -18,16 +18,19 @@ use axum::{ response::Redirect, routing::{get, post, Router}, }; +use diesel::connection::SimpleConnection; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use dotenvy::dotenv; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::OpenFlags as of; use std::env; use tower_http::trace::{DefaultOnResponse, TraceLayer}; use tower_http::LatencyUnit; use tracing::{info, Level}; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + pub(crate) type Result = std::result::Result; #[tokio::main] @@ -46,21 +49,27 @@ async fn main() { .with(fmt::layer()) .init(); - let db_url = env::var("DATABASE_FILE").unwrap(); - println!("DATABASE_FILE={db_url}"); + let database_url = env::var("DATABASE_URL").unwrap(); + println!("DATABASE_URL={database_url}"); - let manager = SqliteConnectionManager::file(db_url.as_str()) - .with_flags(of::SQLITE_OPEN_URI | of::SQLITE_OPEN_CREATE | of::SQLITE_OPEN_READ_WRITE) - .with_init(|conn| conn.pragma_update(None, "journal_mode", "wal")) - .with_init(|conn| conn.pragma_update(None, "synchronous", "normal")) - .with_init(|conn| conn.pragma_update(None, "foreign_keys", "on")); + let manager = ConnectionManager::::new(database_url); let pool = Pool::builder() .max_size(10) .build(manager) - .expect("unable to build pool"); + .expect("Could not build connection pool"); let mut conn = pool.get().unwrap(); - migrations::MIGRATIONS.to_latest(&mut conn).unwrap(); + + conn.batch_execute(" + PRAGMA journal_mode = WAL; -- better write-concurrency + PRAGMA synchronous = NORMAL; -- fsync only in critical moments + PRAGMA wal_autocheckpoint = 1000; -- write WAL changes back every 1000 pages, for an in average 1MB WAL file. May affect readers if number is increased + PRAGMA wal_checkpoint(TRUNCATE); -- free some space by truncating possibly massive WAL files from the last run. + PRAGMA busy_timeout = 250; -- sleep if the database is busy + PRAGMA foreign_keys = ON; -- enforce foreign keys + ").unwrap(); + + conn.run_pending_migrations(MIGRATIONS).unwrap(); drop(conn); let trace_layer = TraceLayer::new_for_http().on_response( diff --git a/src/migrations.rs b/src/migrations.rs deleted file mode 100644 index 467797f..0000000 --- a/src/migrations.rs +++ /dev/null @@ -1,21 +0,0 @@ -use once_cell::sync::Lazy; -use rusqlite_migration::{Migrations, M}; - -pub(crate) static MIGRATIONS: Lazy> = Lazy::new(|| { - Migrations::new(vec![ - M::up(include_str!("../migrations/20230408081854_quotes.up.sql")) - .down(include_str!("../migrations/20230408081854_quotes.down.sql")), - M::up(include_str!( - "../migrations/20230413082629_line_item_dates.up.sql" - )) - .down(include_str!( - "../migrations/20230413082629_line_item_dates.down.sql" - )), - M::up(include_str!( - "../migrations/20230413082726_line_items.up.sql" - )) - .down(include_str!( - "../migrations/20230413082726_line_items.down.sql" - )), - ]) -}); diff --git a/src/quotes/controller.rs b/src/quotes/controller.rs index de2a9ca..20840ea 100644 --- a/src/quotes/controller.rs +++ b/src/quotes/controller.rs @@ -14,16 +14,16 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse}, }; +use diesel::prelude::SqliteConnection; +use diesel::r2d2::{ConnectionManager, Pool}; use hotwire_turbo_axum::TurboStream; use itertools::Itertools; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; use std::time::Instant; use tracing::info; use validator::Validate; pub(crate) async fn index( - State(pool): State>, + State(pool): State>>, ) -> Result> { let start = Instant::now(); let quotes = quotes::query::all(&pool) @@ -45,7 +45,7 @@ pub(crate) async fn index( } pub(crate) async fn show( - State(pool): State>, + State(pool): State>>, Path(id): Path, ) -> Result { let start = Instant::now(); @@ -98,7 +98,7 @@ pub(crate) async fn new() -> impl IntoResponse { } pub(crate) async fn create( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -137,7 +137,7 @@ pub(crate) async fn create( } pub(crate) async fn edit( - State(pool): State>, + State(pool): State>>, Path(id): Path, ) -> Result { let start = Instant::now(); @@ -155,7 +155,7 @@ pub(crate) async fn edit( } pub(crate) async fn update( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let result = form.validate(); @@ -194,7 +194,7 @@ pub(crate) async fn update( } pub(crate) async fn delete( - State(pool): State>, + State(pool): State>>, axum::Form(form): axum::Form, ) -> Result { let start = Instant::now(); diff --git a/src/quotes/model.rs b/src/quotes/model.rs index 066b4f9..7798b1e 100644 --- a/src/quotes/model.rs +++ b/src/quotes/model.rs @@ -1,14 +1,31 @@ +use crate::schema::quotes; use currency_rs::Currency; -use nanoid::nanoid; +use diesel::prelude::*; +use diesel::sql_types::*; use serde::Deserialize; use time::OffsetDateTime; +use ulid::Ulid; use validator::Validate; -#[derive(Debug)] -pub struct Quote { +#[derive(Debug, QueryableByName)] +pub struct QuoteWithTotal { + #[diesel(sql_type = Text)] pub id: String, + #[diesel(sql_type = Text)] pub name: String, + #[diesel(sql_type = currency_rs::diesel2::sqlite::sql_types::Currency)] pub total: Currency, + #[diesel(sql_type = TimestamptzSqlite)] + pub created_at: OffsetDateTime, + #[diesel(sql_type = TimestamptzSqlite)] + pub updated_at: OffsetDateTime, +} + +#[derive(Debug, Insertable, Queryable)] +#[diesel(table_name = quotes)] +pub struct Quote { + pub id: String, + pub name: String, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, } @@ -16,9 +33,8 @@ pub struct Quote { impl From<&QuoteForm> for Quote { fn from(value: &QuoteForm) -> Self { Quote { - id: value.id.clone().unwrap_or(nanoid!()), + id: value.id.clone().unwrap_or(Ulid::new().to_string()), name: value.name.clone(), - total: Currency::new_float(0f64, None), created_at: OffsetDateTime::now_utc(), updated_at: OffsetDateTime::now_utc(), } @@ -69,6 +85,16 @@ impl Default for QuotePresenter { impl From for QuotePresenter { fn from(value: Quote) -> Self { + QuotePresenter { + id: Some(value.id), + name: value.name, + total: Currency::new_float(0f64, None), + } + } +} + +impl From for QuotePresenter { + fn from(value: QuoteWithTotal) -> Self { QuotePresenter { id: Some(value.id), name: value.name, diff --git a/src/quotes/query.rs b/src/quotes/query.rs index f24ecc7..f9f8477 100644 --- a/src/quotes/query.rs +++ b/src/quotes/query.rs @@ -1,48 +1,32 @@ use crate::{ - currency::CurrencySql, line_item_dates, - quotes::model::{Quote, QuoteForm}, - time::DateTimeSql, + quotes::model::{Quote, QuoteForm, QuoteWithTotal}, + schema::quotes, Result, }; -use currency_rs::Currency; -use r2d2::{Pool, PooledConnection}; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::Row; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -pub(crate) async fn all(pool: &Pool) -> Result> { - // language=SQL - let sql = r#" - select - id, - name, - created_at, - updated_at - from quotes - order by rowid desc - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - let records = statement - .query_map([], map_all) - .unwrap() - .map(|result| result.unwrap()) - .collect(); +pub(crate) async fn all(pool: &Pool>) -> Result> { + let mut connection = pool.get()?; + let records = quotes::table + .order_by(quotes::id) + .get_results(&mut connection)?; Ok(records) } pub(crate) async fn read>( - pool: &Pool, + pool: &Pool>, id: S, -) -> Result { - let connection = pool.get()?; - read_from_connection(&connection, id) +) -> Result { + let mut connection = pool.get()?; + read_from_connection(&mut connection, id) } fn read_from_connection>( - connection: &PooledConnection, + connection: &mut PooledConnection>, id: S, -) -> Result { +) -> Result { // language=SQL let sql = r#" select @@ -58,22 +42,23 @@ fn read_from_connection>( from quotes q where q.id = ? "#; - let mut statement = connection.prepare_cached(sql)?; - let record = statement.query_row([id.as_ref()], map_read)?; + let record = diesel::dsl::sql_query(sql) + .bind::(id.as_ref()) + .get_result(connection)?; Ok(record) } pub(crate) async fn from_line_item_date_id>( - pool: &Pool, + pool: &Pool>, id: S, -) -> Result { +) -> Result { // language=SQL let sql = r#" select q.id, q.name, (select - coalesce(sum(quantity * li.unit_price), 0) + coalesce(sum(li.quantity * li.unit_price), 0) from line_items li inner join line_item_dates lid2 on li.line_item_date_id = lid2.id where lid2.quote_id = q.id) as total, @@ -83,91 +68,60 @@ pub(crate) async fn from_line_item_date_id>( inner join quotes q on lid.quote_id = q.id where lid.id = ? "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - let record = statement.query_row([id.as_ref()], map_read)?; + let mut connection = pool.get()?; + let record = diesel::dsl::sql_query(sql) + .bind::(id.as_ref()) + .get_result(&mut connection)?; Ok(record) } pub(crate) async fn insert( - pool: &Pool, + pool: &Pool>, form: &QuoteForm, ) -> Result { let record: Quote = form.into(); - // language=SQL - let sql = r#" - insert into quotes - (id, name, created_at, updated_at) - values - (?, ?, ?, ?); - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute(( - &record.id, - &record.name, - &DateTimeSql(record.created_at), - &DateTimeSql(record.updated_at), - ))?; + let mut connection = pool.get()?; + diesel::dsl::insert_into(quotes::table) + .values(&record) + .execute(&mut connection)?; Ok(record) } pub(crate) async fn update( - pool: &Pool, + pool: &Pool>, form: &QuoteForm, -) -> Result { +) -> Result { let record: Quote = form.into(); - // language=SQL - let sql = r#" - update quotes - set name = ?, - updated_at = ? - where id = ?; - "#; - let connection = pool.get()?; - let mut statement = connection.prepare_cached(sql)?; - statement.execute((&record.name, &DateTimeSql(record.updated_at), &record.id))?; - read_from_connection(&connection, &record.id) + + let mut connection = pool.get()?; + diesel::dsl::update(quotes::table) + .set(( + quotes::name.eq(&record.name), + quotes::updated_at.eq(&record.updated_at), + )) + .filter(quotes::id.eq(&record.id)) + .execute(&mut connection)?; + + read_from_connection(&mut connection, &record.id) } pub(crate) async fn delete>( - pool: &Pool, + pool: &Pool>, id: S, -) -> Result { - let record = read(pool, &id).await?; - // language=SQL - let sql = r#"delete from quotes where id = ?"#; +) -> Result { let mut connection = pool.get()?; - let mut tx = connection.transaction()?; - line_item_dates::query::delete_all_for_quote(&mut tx, &id)?; - { - let mut statement = tx.prepare_cached(sql)?; - statement.execute([&id.as_ref()])?; - } - tx.commit()?; - Ok(record) -} + let record = read_from_connection(&mut connection, &id)?; -#[inline] -fn map_all(row: &Row<'_>) -> rusqlite::Result { - Ok(Quote { - id: row.get(0)?, - name: row.get(1)?, - total: Currency::new_float(0f64, None), - created_at: row.get::<_, DateTimeSql>(2)?.0, - updated_at: row.get::<_, DateTimeSql>(3)?.0, - }) -} + _ = connection.transaction::<_, _, _>(|tx| { + line_item_dates::query::delete_all_for_quote(tx, &id)?; + + _ = diesel::dsl::delete(quotes::table) + .filter(quotes::id.eq(&id.as_ref())) + .execute(tx)?; -#[inline] -fn map_read(row: &Row<'_>) -> rusqlite::Result { - Ok(Quote { - id: row.get(0)?, - name: row.get(1)?, - total: row.get::<_, CurrencySql>(2)?.0, - created_at: row.get::<_, DateTimeSql>(3)?.0, - updated_at: row.get::<_, DateTimeSql>(4)?.0, - }) + Ok::<(), crate::error::AppError>(()) + }); + Ok(record) } diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..c69127e --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,41 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + line_item_dates (id) { + id -> Text, + quote_id -> Text, + date -> Date, + created_at -> TimestamptzSqlite, + updated_at -> TimestamptzSqlite, + } +} + +diesel::table! { + use currency_rs::diesel2::sqlite::sql_types::Currency; + use diesel::sql_types::*; + + line_items (id) { + id -> Text, + line_item_date_id -> Text, + name -> Text, + description -> Nullable, + quantity -> Integer, + unit_price -> Currency, + created_at -> TimestamptzSqlite, + updated_at -> TimestamptzSqlite, + } +} + +diesel::table! { + quotes (id) { + id -> Text, + name -> Text, + created_at -> TimestamptzSqlite, + updated_at -> TimestamptzSqlite, + } +} + +diesel::joinable!(line_item_dates -> quotes (quote_id)); +diesel::joinable!(line_items -> line_item_dates (line_item_date_id)); + +diesel::allow_tables_to_appear_in_same_query!(line_item_dates, line_items, quotes); diff --git a/src/schema.rs.patch b/src/schema.rs.patch new file mode 100644 index 0000000..15a2465 --- /dev/null +++ b/src/schema.rs.patch @@ -0,0 +1,57 @@ +--- src/schema.rs.unpatched 2023-11-01 20:17:20 ++++ src/schema.rs 2023-11-01 20:19:24 +@@ -1,42 +1,41 @@ + // @generated automatically by Diesel CLI. + + diesel::table! { + line_item_dates (id) { + id -> Text, + quote_id -> Text, +- date -> Text, +- created_at -> Text, +- updated_at -> Text, ++ date -> Date, ++ created_at -> TimestamptzSqlite, ++ updated_at -> TimestamptzSqlite, + } + } + + diesel::table! { ++ use currency_rs::diesel2::sqlite::sql_types::Currency; ++ use diesel::sql_types::*; ++ + line_items (id) { + id -> Text, + line_item_date_id -> Text, + name -> Text, + description -> Nullable, + quantity -> Integer, +- unit_price -> Double, +- created_at -> Text, +- updated_at -> Text, ++ unit_price -> Currency, ++ created_at -> TimestamptzSqlite, ++ updated_at -> TimestamptzSqlite, + } + } + + diesel::table! { + quotes (id) { + id -> Text, + name -> Text, +- created_at -> Text, +- updated_at -> Text, ++ created_at -> TimestamptzSqlite, ++ updated_at -> TimestamptzSqlite, + } + } + + diesel::joinable!(line_item_dates -> quotes (quote_id)); + diesel::joinable!(line_items -> line_item_dates (line_item_date_id)); + +-diesel::allow_tables_to_appear_in_same_query!( +- line_item_dates, +- line_items, +- quotes, +-); ++diesel::allow_tables_to_appear_in_same_query!(line_item_dates, line_items, quotes); diff --git a/src/schema.rs.unpatched b/src/schema.rs.unpatched new file mode 100644 index 0000000..2ba02ff --- /dev/null +++ b/src/schema.rs.unpatched @@ -0,0 +1,42 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + line_item_dates (id) { + id -> Text, + quote_id -> Text, + date -> Text, + created_at -> Text, + updated_at -> Text, + } +} + +diesel::table! { + line_items (id) { + id -> Text, + line_item_date_id -> Text, + name -> Text, + description -> Nullable, + quantity -> Integer, + unit_price -> Double, + created_at -> Text, + updated_at -> Text, + } +} + +diesel::table! { + quotes (id) { + id -> Text, + name -> Text, + created_at -> Text, + updated_at -> Text, + } +} + +diesel::joinable!(line_item_dates -> quotes (quote_id)); +diesel::joinable!(line_items -> line_item_dates (line_item_date_id)); + +diesel::allow_tables_to_appear_in_same_query!( + line_item_dates, + line_items, + quotes, +); diff --git a/src/time.rs b/src/time.rs index 2a9841a..92fb12a 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,63 +1,11 @@ use once_cell::sync::Lazy; use regex::Regex; -use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; -use rusqlite::Result; -use time::{ - format_description::{well_known::Rfc3339, FormatItem}, - macros::format_description, - Date, OffsetDateTime, -}; +use time::{format_description::FormatItem, macros::format_description, Date}; pub(crate) static DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); pub(crate) static DATE_REGEX: Lazy = Lazy::new(|| Regex::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2}$").unwrap()); -pub(crate) struct DateTimeSql(pub(crate) OffsetDateTime); - -impl FromSql for DateTimeSql { - fn column_result(value: ValueRef) -> FromSqlResult { - String::column_result(value).and_then(|as_string| { - OffsetDateTime::parse(as_string.as_ref(), &Rfc3339) - .map(DateTimeSql) - .map_err(|err| FromSqlError::Other(Box::new(err))) - }) - } -} - -impl ToSql for DateTimeSql { - //noinspection DuplicatedCode - fn to_sql(&self) -> Result { - let time_string = self - .0 - .format(&Rfc3339) - .map_err(|err| FromSqlError::Other(Box::new(err)))?; - Ok(ToSqlOutput::from(time_string)) - } -} - -pub(crate) struct DateSql(pub(crate) Date); - -impl FromSql for DateSql { - fn column_result(value: ValueRef) -> FromSqlResult { - String::column_result(value).and_then(|as_string| { - Date::parse(as_string.as_ref(), &DATE_FORMAT) - .map(DateSql) - .map_err(|err| FromSqlError::Other(Box::new(err))) - }) - } -} - -impl ToSql for DateSql { - //noinspection DuplicatedCode - fn to_sql(&self) -> Result { - let date_string = self - .0 - .format(&DATE_FORMAT) - .map_err(|err| FromSqlError::Other(Box::new(err)))?; - Ok(ToSqlOutput::from(date_string)) - } -} - pub(crate) fn long_form(date: Date) -> String { let mut result = String::with_capacity(18); result.push_str(date.month().to_string().as_str());