From 481fb6f894d5f30e63f2c0ade015693b34566fa8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Mon, 1 Apr 2024 18:22:08 +0800 Subject: [PATCH] Add Derive FieldType support for newtypes --- .editorconfig | 3 + Cargo.lock | 307 +++++++++----- Cargo.toml | 2 +- butane_codegen/src/lib.rs | 61 ++- butane_core/Cargo.toml | 1 + butane_core/src/codegen/mod.rs | 2 +- butane_core/src/lib.rs | 2 +- docs/newtype.md | 141 +++++++ examples/newtype/.butane/clistate.json | 3 + .../newtype/.butane/migrations/.gitignore | 1 + .../20240401_095709389_init/Blog.table | 31 ++ .../20240401_095709389_init/Post.table | 102 +++++ .../Post_tags_Many.table | 43 ++ .../20240401_095709389_init/Tag.table | 18 + .../20240401_095709389_init/info.json | 6 + .../20240401_095709389_init/pg_down.sql | 4 + .../20240401_095709389_init/pg_up.sql | 26 ++ .../20240401_095709389_init/sqlite_down.sql | 4 + .../20240401_095709389_init/sqlite_up.sql | 28 ++ .../20240406_035726416_tags/Post.table | 115 +++++ .../20240406_035726416_tags/info.json | 10 + .../20240406_035726416_tags/pg_down.sql | 10 + .../20240406_035726416_tags/pg_up.sql | 3 + .../20240406_035726416_tags/sqlite_down.sql | 22 + .../20240406_035726416_tags/sqlite_up.sql | 3 + .../.butane/migrations/current/Blog.table | 27 ++ .../.butane/migrations/current/Post.table | 108 +++++ .../.butane/migrations/current/types.json | 1 + .../newtype/.butane/migrations/state.json | 3 + examples/newtype/.gitignore | 2 + examples/newtype/Cargo.toml | 35 ++ examples/newtype/README.md | 8 + examples/newtype/src/butane_migrations.rs | 397 ++++++++++++++++++ examples/newtype/src/lib.rs | 42 ++ examples/newtype/src/models.rs | 82 ++++ examples/newtype/tests/rollback.rs | 61 +++ examples/newtype/tests/validation.rs | 7 + 37 files changed, 1616 insertions(+), 105 deletions(-) create mode 100644 docs/newtype.md create mode 100644 examples/newtype/.butane/clistate.json create mode 100644 examples/newtype/.butane/migrations/.gitignore create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/Blog.table create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/Post.table create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/Post_tags_Many.table create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/Tag.table create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/info.json create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/pg_down.sql create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/pg_up.sql create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_down.sql create mode 100644 examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_up.sql create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/Post.table create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/info.json create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/pg_down.sql create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/pg_up.sql create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_down.sql create mode 100644 examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_up.sql create mode 100644 examples/newtype/.butane/migrations/current/Blog.table create mode 100644 examples/newtype/.butane/migrations/current/Post.table create mode 100644 examples/newtype/.butane/migrations/current/types.json create mode 100644 examples/newtype/.butane/migrations/state.json create mode 100644 examples/newtype/.gitignore create mode 100644 examples/newtype/Cargo.toml create mode 100644 examples/newtype/README.md create mode 100644 examples/newtype/src/butane_migrations.rs create mode 100644 examples/newtype/src/lib.rs create mode 100644 examples/newtype/src/models.rs create mode 100644 examples/newtype/tests/rollback.rs create mode 100644 examples/newtype/tests/validation.rs diff --git a/.editorconfig b/.editorconfig index 3920420b..299cce5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,5 +12,8 @@ max_line_length = 400 [butane_migrations.rs] max_line_length = unset +[types.json] +max_line_length = unset + [README.md] max_line_length = 200 diff --git a/Cargo.lock b/Cargo.lock index a4cb2ae3..3cf629e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "approx" @@ -145,13 +145,13 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -162,15 +162,15 @@ checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -195,9 +195,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -254,7 +254,7 @@ dependencies = [ "once_cell", "paste", "postgres", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", "r2d2", "rand", @@ -288,9 +288,9 @@ name = "butane_codegen" version = "0.6.1" dependencies = [ "butane_core", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -317,7 +317,7 @@ dependencies = [ "pin-project", "postgres", "postgres-native-tls", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", "r2d2", "rand", @@ -326,7 +326,8 @@ dependencies = [ "serde", "serde_json", "sqlparser", - "syn 2.0.52", + "strum", + "syn 2.0.58", "tempfile", "thiserror", "uuid", @@ -356,9 +357,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" @@ -371,9 +372,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -392,6 +393,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.90" @@ -406,9 +416,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -419,9 +429,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -446,20 +456,20 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", "terminal_size", ] [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", - "proc-macro2 1.0.78", + "heck 0.5.0", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -474,6 +484,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -551,10 +574,10 @@ checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", "strsim 0.10.0", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -576,7 +599,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -615,9 +638,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e57e12b69e57fad516e01e2b3960f122696fdb13420e1a88ed8e210316f2876" dependencies = [ "darling 0.20.8", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -706,9 +729,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "finl_unicode" @@ -801,9 +824,9 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -836,6 +859,28 @@ dependencies = [ "slab", ] +[[package]] +name = "garde" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fa8fb3ffe035745c6194540b2064b2fe275f32367fbb4eb026024b7921e2e5" +dependencies = [ + "compact_str", + "garde_derive", + "smallvec", +] + +[[package]] +name = "garde_derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf62650515830c41553b72bd49ec20fb120226f9277c7f2847f071cf998325b" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.58", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -859,9 +904,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" dependencies = [ "cfg-if", "libc", @@ -913,6 +958,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -965,9 +1016,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -1041,9 +1092,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miniz_oxide" @@ -1083,6 +1134,25 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newtype" +version = "0.1.0" +dependencies = [ + "butane", + "butane_cli", + "butane_core", + "butane_test_helper", + "cfg-if", + "env_logger", + "fake", + "garde", + "log", + "paste", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "nonempty" version = "0.10.0" @@ -1120,7 +1190,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1135,9 +1205,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -1148,9 +1218,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1226,16 +1296,16 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1353,9 +1423,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1375,7 +1445,7 @@ version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", ] [[package]] @@ -1439,9 +1509,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1462,9 +1532,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rusqlite" @@ -1472,7 +1542,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", @@ -1489,17 +1559,23 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.17" @@ -1532,9 +1608,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1545,9 +1621,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -1577,16 +1653,16 @@ version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1627,9 +1703,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -1650,6 +1726,12 @@ dependencies = [ "log", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.4" @@ -1675,9 +1757,31 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.79", + "quote 1.0.35", + "rustversion", + "syn 2.0.58", +] [[package]] name = "subtle" @@ -1698,11 +1802,11 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", "unicode-ident", ] @@ -1737,22 +1841,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -1772,9 +1876,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -1895,13 +1999,14 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "atomic", "getrandom", "md-5", + "serde", "sha1_smol", ] @@ -1957,9 +2062,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -1979,9 +2084,9 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2004,9 +2109,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ "redox_syscall", "wasite", @@ -2191,7 +2296,7 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ - "proc-macro2 1.0.78", + "proc-macro2 1.0.79", "quote 1.0.35", - "syn 2.0.52", + "syn 2.0.58", ] diff --git a/Cargo.toml b/Cargo.toml index cda1c49a..6280cd54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "butane_core", "butane_test_helper", "example", - "examples/getting_started", + "examples/*", ] [workspace.package] diff --git a/butane_codegen/src/lib.rs b/butane_codegen/src/lib.rs index 08468ebf..4f327935 100644 --- a/butane_codegen/src/lib.rs +++ b/butane_codegen/src/lib.rs @@ -240,12 +240,71 @@ pub fn derive_field_type(input: TokenStream) -> TokenStream { let derive_input = syn::parse_macro_input!(input as syn::DeriveInput); let ident = &derive_input.ident; match derive_input.data { - syn::Data::Struct(_) => derive_field_type_with_json(ident), + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed, .. }), + .. + }) => { + if unnamed.len() == 1 { + let field = unnamed.first().unwrap(); + if let Some(DeferredSqlType::KnownId(TypeIdentifier::Ty(sqltype))) = + codegen::get_primitive_sql_type(&field.ty) + { + return derive_field_type_for_newtype(ident, sqltype); + } + } + derive_field_type_with_json(ident) + } + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named { .. }, + .. + }) + | syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Unit { .. }, + .. + }) => derive_field_type_with_json(ident), syn::Data::Enum(data_enum) => derive_field_type_for_enum(ident, data_enum), syn::Data::Union(_) => derive_field_type_with_json(ident), } } +fn derive_field_type_for_newtype(ident: &Ident, sqltype: SqlType) -> TokenStream { + let sqltype_name: &'static str = sqltype.clone().into(); + let sqltype_ident = syn::Ident::new(sqltype_name, proc_macro2::Span::call_site()); + + let mut migrations = migrations_for_dir(); + codegen::add_custom_type( + &mut migrations, + ident.to_string(), + DeferredSqlType::KnownId(TypeIdentifier::Ty(sqltype)), + ) + .unwrap(); + + quote!( + impl butane::ToSql for #ident + { + fn to_sql(&self) -> butane::SqlVal { + self.0.to_sql() + } + fn to_sql_ref(&self) -> butane::SqlValRef<'_> { + self.0.to_sql_ref() + } + } + impl butane::FromSql for #ident + { + fn from_sql_ref(val: butane::SqlValRef) -> std::result::Result { + let inner = butane::FromSql::from_sql_ref(val)?; + Ok(Self ( inner )) + } + } + impl butane::FieldType for #ident + { + type RefType = Self; + const SQLTYPE: butane::SqlType = butane::SqlType:: #sqltype_ident; + } + ) + .into() +} + fn derive_field_type_for_enum(ident: &Ident, data_enum: syn::DataEnum) -> TokenStream { if data_enum .variants diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 8aaebd05..0b45b9bf 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -45,6 +45,7 @@ regex = { version = "1.5", features = ["std"] } rusqlite = { workspace = true, optional = true } serde = { features = ["derive"], workspace = true } serde_json = { workspace = true } +strum = { version = "0.26", features = ["derive"] } sqlparser = { workspace = true } syn = { workspace = true } thiserror = "1.0" diff --git a/butane_core/src/codegen/mod.rs b/butane_core/src/codegen/mod.rs index 6c2f2e04..b184269d 100644 --- a/butane_core/src/codegen/mod.rs +++ b/butane_core/src/codegen/mod.rs @@ -442,7 +442,7 @@ fn some_known(ty: SqlType) -> Option { } /// If the field refers to a primitive, return its SqlType -fn get_primitive_sql_type(ty: &syn::Type) -> Option { +pub fn get_primitive_sql_type(ty: &syn::Type) -> Option { if *ty == parse_quote!(bool) { return some_known(SqlType::Bool); } else if *ty == parse_quote!(u8) diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 7213c4e0..d869259f 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -268,7 +268,7 @@ impl From for Error { /// Enumeration of the types a database value may take. /// /// See also [`SqlVal`]. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, strum::IntoStaticStr)] pub enum SqlType { /// Boolean Bool, diff --git a/docs/newtype.md b/docs/newtype.md new file mode 100644 index 00000000..515327cb --- /dev/null +++ b/docs/newtype.md @@ -0,0 +1,141 @@ +# Newtype support + +This guide builds on the `getting_started` walkthrough, using the +[newtype pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html) +with Butane to utilise types that are not natively supported by Butane. + +It again builds the database portions of a blog, so that the base models +are not new, and the focus can be on the additional types used. + +Let's begin by creating a new rust project + +``` shell +cargo new --lib newtype && cd newtype +``` + +In `Cargo.toml`, add a dependency on Butane, and a few other types we'll use: + +``` toml +[dependencies] +butane = { version = "0.6", features=["pg", "sqlite"] } +uuid = { version = "1.8", features = ["serde", "v4"] } +``` + +This guide will use SQLite initially, and use "pg" for +PostgreSQL support at the end. + +Again, we initialise the butane metadata with + +``` shell +cargo install butane_cli +butane init sqlite example.db +``` + +Copy the `lib.rs` from the `getting_started` example into `src/`. + +## Wrapping supported types + +### Uuids + +Let's enhance the `Blog` and `Post` models to have dedicated primary key types, and are a `uuid`. + +Create `src/models.rs` with a `BlogId` and `PostId` struct wrapping a `uuid`. + +``` rust +use butane::{FieldType, PrimaryKeyType}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, FieldType, PartialEq, Eq)] +pub struct BlogId(pub uuid::Uuid); +impl PrimaryKeyType for BlogId {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, FieldType, PartialEq, Eq)] +pub struct PostId(pub uuid::Uuid); +impl PrimaryKeyType for PostId {} +``` + +For each, `FieldType` is derived, and they implement the marker trait `PrimaryKeyType` +to allow their use as a primary key. + +As Butane natively supports `uuid`, these newtypes will be stored in the butane metadata as +"Blob" type, which is stored in the database using an appropriate column type based on the +database's supported columns. For `uuid`, this is a `BLOB` on SQLite, and `BYTEA` on PostgreSQL. + +If we compile this project now, a `.butane/migrations/current/types.json` will be generated +with the following contents: + +``` json +{"CT:BlogId":{"KnownId":{"Ty":"Blob"}},"CT:PostId":{"KnownId":{"Ty":"Blob"}}} +``` + +Now we can add `Blog` and `Post`, which can use these types for their primary key. + +``` rust +#[model] +#[derive(Debug, Default)] +pub struct Blog { + pub id: BlogId, + // ... +} + +#[model] +pub struct Post { + pub id: PostId, + // ... +} +``` + +Now it is impossible to accidentally use a `BlogId` in conjunction with `Post.id`. + +### Strings + +We know that Unicode contains lots of [Homoglyph](https://en.wikipedia.org/wiki/Homoglyph), +so a simple way to avoid two blogs having indistinguishable names is to require the name is ASCII. + +We can use [`garde`](https://crates.io/crates/garde) to add validation. + +``` rust +#[derive(Clone, Debug, Default, Deserialize, Dummy, Eq, FieldType, PartialEq, Serialize, Validate)] +pub struct BlogName(#[garde(ascii)] String); +``` + +If we compile this project now, `.butane/migrations/current/types.json` will contain a new entry +`CT:BlogName`, which is of type "Text": + +``` json +{"CT:BlogId":{"KnownId":{"Ty":"Blob"}},"CT:BlogName":{"KnownId":{"Ty":"Text"}},"CT:PostId":{"KnownId":{"Ty":"Blob"}}} +``` + +### Unsupported types + +The previous two "newtypes" wrapped types supported by Butane. +When Butane does not support the inner type, `#[derive(FieldType)]` +will fallback to storing the type in JSON. + +We can use this to implement the blog post tags without a separate table, using `HashSet`. + +``` rust +#[derive(Clone, Debug, Default, Deserialize, Dummy, Eq, FieldType, PartialEq, Serialize)] +pub struct Tags(pub std::collections::HashSet); +``` + +If we compile this project now, `.butane/migrations/current/types.json` will contain a new entry +`CT:Tags`, which is of type "Json": + +``` json +{"CT:BlogId":{"KnownId":{"Ty":"Blob"}},"CT:BlogName":{"KnownId":{"Ty":"Text"}},"CT:PostId":{"KnownId":{"Ty":"Blob"}}} +``` + +In SQLite, this will be stored in a "TEXT" column, while on PostgreSQL it +will use the "JSONB" column type. + +This can now be used in the Post struct: + +``` rust +#[model] +pub struct Post { + // .. + pub tags: Tags, + // .. +} +``` diff --git a/examples/newtype/.butane/clistate.json b/examples/newtype/.butane/clistate.json new file mode 100644 index 00000000..a8f08861 --- /dev/null +++ b/examples/newtype/.butane/clistate.json @@ -0,0 +1,3 @@ +{ + "embedded": true +} diff --git a/examples/newtype/.butane/migrations/.gitignore b/examples/newtype/.butane/migrations/.gitignore new file mode 100644 index 00000000..4b67b6a0 --- /dev/null +++ b/examples/newtype/.butane/migrations/.gitignore @@ -0,0 +1 @@ +lock diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/Blog.table b/examples/newtype/.butane/migrations/20240401_095709389_init/Blog.table new file mode 100644 index 00000000..9cf7fa1a --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/Blog.table @@ -0,0 +1,31 @@ +{ + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/Post.table b/examples/newtype/.butane/migrations/20240401_095709389_init/Post.table new file mode 100644 index 00000000..b4a7f8a4 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/Post.table @@ -0,0 +1,102 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/Post_tags_Many.table b/examples/newtype/.butane/migrations/20240401_095709389_init/Post_tags_Many.table new file mode 100644 index 00000000..9a703b89 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/Post_tags_Many.table @@ -0,0 +1,43 @@ +{ + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Post", + "column_name": "id" + } + } + }, + { + "name": "has", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Tag", + "column_name": "tag" + } + } + } + ] +} diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/Tag.table b/examples/newtype/.butane/migrations/20240401_095709389_init/Tag.table new file mode 100644 index 00000000..784abd8b --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/Tag.table @@ -0,0 +1,18 @@ +{ + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/info.json b/examples/newtype/.butane/migrations/20240401_095709389_init/info.json new file mode 100644 index 00000000..beabd46e --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/info.json @@ -0,0 +1,6 @@ +{ + "backends": [ + "sqlite", + "pg" + ] +} diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/pg_down.sql b/examples/newtype/.butane/migrations/20240401_095709389_init/pg_down.sql new file mode 100644 index 00000000..a565ac28 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/pg_down.sql @@ -0,0 +1,4 @@ +DROP TABLE Blog; +DROP TABLE Post; +DROP TABLE Post_tags_Many; +DROP TABLE Tag; diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/pg_up.sql b/examples/newtype/.butane/migrations/20240401_095709389_init/pg_up.sql new file mode 100644 index 00000000..d468cecf --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/pg_up.sql @@ -0,0 +1,26 @@ +CREATE TABLE Blog ( +id BYTEA NOT NULL PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE TABLE Post ( +id BYTEA NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published BOOLEAN NOT NULL, +blog BYTEA NOT NULL, +byline TEXT , +likes INTEGER NOT NULL +); +CREATE TABLE Post_tags_Many ( +owner BYTEA NOT NULL, +has TEXT NOT NULL +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); +ALTER TABLE Post ADD FOREIGN KEY (blog) REFERENCES Blog(id); +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id); +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag); +CREATE TABLE IF NOT EXISTS butane_migrations ( +"name" TEXT NOT NULL PRIMARY KEY +); diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_down.sql b/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_down.sql new file mode 100644 index 00000000..a565ac28 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_down.sql @@ -0,0 +1,4 @@ +DROP TABLE Blog; +DROP TABLE Post; +DROP TABLE Post_tags_Many; +DROP TABLE Tag; diff --git a/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_up.sql b/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_up.sql new file mode 100644 index 00000000..83ef877b --- /dev/null +++ b/examples/newtype/.butane/migrations/20240401_095709389_init/sqlite_up.sql @@ -0,0 +1,28 @@ +CREATE TABLE Blog ( +id BLOB NOT NULL PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE TABLE Post ( +id BLOB NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog BLOB NOT NULL, +byline TEXT, +likes INTEGER NOT NULL, +FOREIGN KEY (blog) REFERENCES Blog(id) +); +CREATE TABLE Post_tags_Many ( +owner BLOB NOT NULL, +has TEXT NOT NULL, +FOREIGN KEY (owner) REFERENCES Post(id) +FOREIGN KEY (has) REFERENCES Tag(tag) +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); + + +CREATE TABLE IF NOT EXISTS butane_migrations ( +"name" TEXT NOT NULL PRIMARY KEY +); diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/Post.table b/examples/newtype/.butane/migrations/20240406_035726416_tags/Post.table new file mode 100644 index 00000000..e307ec31 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/Post.table @@ -0,0 +1,115 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "tags", + "sqltype": { + "KnownId": { + "Ty": "Json" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/info.json b/examples/newtype/.butane/migrations/20240406_035726416_tags/info.json new file mode 100644 index 00000000..5ba6e037 --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/info.json @@ -0,0 +1,10 @@ +{ + "from_name": "20240401_095709389_init", + "table_bases": { + "Blog": "20240401_095709389_init" + }, + "backends": [ + "sqlite", + "pg" + ] +} diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_down.sql b/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_down.sql new file mode 100644 index 00000000..9138c7bf --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_down.sql @@ -0,0 +1,10 @@ +CREATE TABLE Post_tags_Many ( +owner BYTEA NOT NULL, +has TEXT NOT NULL +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); +ALTER TABLE Post DROP COLUMN tags; +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id); +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag); diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_up.sql b/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_up.sql new file mode 100644 index 00000000..27c2182f --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/pg_up.sql @@ -0,0 +1,3 @@ +DROP TABLE Post_tags_Many; +DROP TABLE Tag; +ALTER TABLE Post ADD COLUMN tags JSONB NOT NULL DEFAULT null; diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_down.sql b/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_down.sql new file mode 100644 index 00000000..41f8c04e --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_down.sql @@ -0,0 +1,22 @@ +CREATE TABLE Post_tags_Many ( +owner BLOB NOT NULL, +has TEXT NOT NULL, +FOREIGN KEY (owner) REFERENCES Post(id) +FOREIGN KEY (has) REFERENCES Tag(tag) +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); +CREATE TABLE Post__butane_tmp ( +id BLOB NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog BLOB NOT NULL, +byline TEXT, +likes INTEGER NOT NULL, +FOREIGN KEY (blog) REFERENCES Blog(id) +); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; diff --git a/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_up.sql b/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_up.sql new file mode 100644 index 00000000..4c5fe94a --- /dev/null +++ b/examples/newtype/.butane/migrations/20240406_035726416_tags/sqlite_up.sql @@ -0,0 +1,3 @@ +DROP TABLE Post_tags_Many; +DROP TABLE Tag; +ALTER TABLE Post ADD COLUMN tags TEXT NOT NULL DEFAULT null; diff --git a/examples/newtype/.butane/migrations/current/Blog.table b/examples/newtype/.butane/migrations/current/Blog.table new file mode 100644 index 00000000..5fb00c90 --- /dev/null +++ b/examples/newtype/.butane/migrations/current/Blog.table @@ -0,0 +1,27 @@ +{ + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "Deferred": "CT:BlogId" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "Deferred": "CT:BlogName" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/current/Post.table b/examples/newtype/.butane/migrations/current/Post.table new file mode 100644 index 00000000..3ae589f7 --- /dev/null +++ b/examples/newtype/.butane/migrations/current/Post.table @@ -0,0 +1,108 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "Deferred": "CT:PostId" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "tags", + "sqltype": { + "Deferred": "CT:Tags" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Deferred": "PK:Blog" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Deferred": { + "Deferred": "PK:Blog" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/newtype/.butane/migrations/current/types.json b/examples/newtype/.butane/migrations/current/types.json new file mode 100644 index 00000000..dff1016d --- /dev/null +++ b/examples/newtype/.butane/migrations/current/types.json @@ -0,0 +1 @@ +{"CT:BlogId":{"KnownId":{"Ty":"Blob"}},"CT:BlogName":{"KnownId":{"Ty":"Text"}},"CT:PostId":{"KnownId":{"Ty":"Blob"}},"CT:Tags":{"KnownId":{"Ty":"Json"}}} diff --git a/examples/newtype/.butane/migrations/state.json b/examples/newtype/.butane/migrations/state.json new file mode 100644 index 00000000..ff6e972a --- /dev/null +++ b/examples/newtype/.butane/migrations/state.json @@ -0,0 +1,3 @@ +{ + "latest": "20240406_035726416_tags" +} diff --git a/examples/newtype/.gitignore b/examples/newtype/.gitignore new file mode 100644 index 00000000..4ff5f9ef --- /dev/null +++ b/examples/newtype/.gitignore @@ -0,0 +1,2 @@ +/example.db +/.butane/connection.json diff --git a/examples/newtype/Cargo.toml b/examples/newtype/Cargo.toml new file mode 100644 index 00000000..7801e764 --- /dev/null +++ b/examples/newtype/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "newtype" +version = "0.1.0" +edition.workspace = true +license = "MIT OR Apache-2.0" +publish = false + +[lib] +doc = false + +[features] +default = ["pg", "sqlite", "sqlite-bundled"] +pg = ["butane/pg"] +sqlite = ["butane/sqlite"] +sqlite-bundled = ["butane/sqlite-bundled"] + +[dependencies] +butane.workspace = true +fake = { workspace = true, features = ["chrono", "derive", "uuid"] } +garde = { version = "*", features = ["derive"] } +serde.workspace = true +serde_json.workspace = true +uuid = { workspace = true, features = ["serde", "v4"] } + +[dev-dependencies] +butane_cli.workspace = true +butane_core.workspace = true +butane_test_helper.workspace = true +cfg-if.workspace = true +env_logger.workspace = true +log.workspace = true +paste.workspace = true + +[package.metadata.release] +release = false diff --git a/examples/newtype/README.md b/examples/newtype/README.md new file mode 100644 index 00000000..310fe6f8 --- /dev/null +++ b/examples/newtype/README.md @@ -0,0 +1,8 @@ +# butane `getting_started_uuid` example + +To use this example, build the entire project using `cargo build` in the project root, +and then run these commands in this directory: + +1. Initialise a Sqlite database using `cargo run -p butane_cli init sqlite db.sqlite` +2. Migrate the new sqlite database using `cargo run -p butane_cli migrate` +3. Run the commands, such as `cargo run --bin write_post_uuid` diff --git a/examples/newtype/src/butane_migrations.rs b/examples/newtype/src/butane_migrations.rs new file mode 100644 index 00000000..56c8b131 --- /dev/null +++ b/examples/newtype/src/butane_migrations.rs @@ -0,0 +1,397 @@ +//! Butane migrations embedded in Rust. + +use butane::migrations::MemMigrations; + +/// Load the butane migrations embedded in Rust. +pub fn get_migrations() -> Result { + let json = r#"{ + "migrations": { + "20240401_095709389_init": { + "name": "20240401_095709389_init", + "db": { + "tables": { + "Blog": { + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post": { + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post_tags_Many": { + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Post", + "column_name": "id" + } + } + }, + { + "name": "has", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Tag", + "column_name": "tag" + } + } + } + ] + }, + "Tag": { + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] + } + }, + "extra_types": {} + }, + "from": null, + "up": { + "pg": "CREATE TABLE Blog (\nid BYTEA NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid BYTEA NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BYTEA NOT NULL,\nbyline TEXT ,\nlikes INTEGER NOT NULL\n);\nCREATE TABLE Post_tags_Many (\nowner BYTEA NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nALTER TABLE Post ADD FOREIGN KEY (blog) REFERENCES Blog(id);\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id);\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n", + "sqlite": "CREATE TABLE Blog (\nid BLOB NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid BLOB NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog BLOB NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL,\nFOREIGN KEY (blog) REFERENCES Blog(id)\n);\nCREATE TABLE Post_tags_Many (\nowner BLOB NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\n\n\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n" + }, + "down": { + "pg": "DROP TABLE Blog;\nDROP TABLE Post;\nDROP TABLE Post_tags_Many;\nDROP TABLE Tag;\n", + "sqlite": "DROP TABLE Blog;\nDROP TABLE Post;\nDROP TABLE Post_tags_Many;\nDROP TABLE Tag;\n" + } + }, + "20240406_035726416_tags": { + "name": "20240406_035726416_tags", + "db": { + "tables": { + "Blog": { + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post": { + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "tags", + "sqltype": { + "KnownId": { + "Ty": "Json" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "Blob" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + } + }, + "extra_types": {} + }, + "from": "20240401_095709389_init", + "up": { + "pg": "DROP TABLE Post_tags_Many;\nDROP TABLE Tag;\nALTER TABLE Post ADD COLUMN tags JSONB NOT NULL DEFAULT null;\n", + "sqlite": "DROP TABLE Post_tags_Many;\nDROP TABLE Tag;\nALTER TABLE Post ADD COLUMN tags TEXT NOT NULL DEFAULT null;\n" + }, + "down": { + "pg": "CREATE TABLE Post_tags_Many (\nowner BYTEA NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nALTER TABLE Post DROP COLUMN tags;\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id);\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag);\n", + "sqlite": "CREATE TABLE Post_tags_Many (\nowner BLOB NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE Post__butane_tmp (\nid BLOB NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog BLOB NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL,\nFOREIGN KEY (blog) REFERENCES Blog(id)\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\n" + } + } + }, + "current": { + "name": "current", + "db": { + "tables": {}, + "extra_types": {} + }, + "from": null, + "up": {}, + "down": {} + }, + "latest": "20240406_035726416_tags" +}"#; + MemMigrations::from_json(json) +} diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs new file mode 100644 index 00000000..fd3b9ace --- /dev/null +++ b/examples/newtype/src/lib.rs @@ -0,0 +1,42 @@ +//! Common helpers for the newtype example. + +#![deny(missing_docs)] + +pub mod butane_migrations; +pub mod models; + +use butane::db::{Connection, ConnectionSpec}; +use butane::migrations::{Migration, Migrations}; +use butane::prelude::*; +use models::{Blog, Post}; + +/// Load a [Connection]. +pub fn establish_connection() -> Connection { + let mut connection = + butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()).unwrap(); + let migrations = butane_migrations::get_migrations().unwrap(); + let to_apply = migrations.unapplied_migrations(&connection).unwrap(); + for migration in to_apply { + migration.apply(&mut connection).unwrap(); + } + connection +} + +/// Create a [Blog]. +pub fn create_blog(conn: &Connection, name: impl Into) -> Blog { + let mut blog = Blog::new(name).unwrap(); + blog.save(conn).unwrap(); + blog +} + +/// Create a [Post]. +pub fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { + let mut new_post = Post::new(blog, title, body); + new_post.save(conn).unwrap(); + new_post +} + +/// Fetch the first existing [Blog] if one exists. +pub fn existing_blog(conn: &Connection) -> Option { + Blog::query().load_first(conn).unwrap() +} diff --git a/examples/newtype/src/models.rs b/examples/newtype/src/models.rs new file mode 100644 index 00000000..e7c2f2e2 --- /dev/null +++ b/examples/newtype/src/models.rs @@ -0,0 +1,82 @@ +//! Models for the newtype example. + +use butane::{model, FieldType, ForeignKey, PrimaryKeyType}; +use garde::Validate; +use serde::{Deserialize, Serialize}; + +/// Blog identifier. +#[derive(Clone, Debug, Default, Deserialize, Eq, FieldType, PartialEq, Serialize)] +pub struct BlogId(pub uuid::Uuid); +impl PrimaryKeyType for BlogId {} + +/// Post identifier. +#[derive(Clone, Debug, Default, Deserialize, Eq, FieldType, PartialEq, Serialize)] +pub struct PostId(pub uuid::Uuid); +impl PrimaryKeyType for PostId {} + +/// Blog name. +#[derive(Clone, Debug, Default, Deserialize, Eq, FieldType, PartialEq, Serialize, Validate)] +pub struct BlogName(#[garde(ascii)] String); + +/// Blog post unique tags. +#[derive(Clone, Debug, Default, Deserialize, Eq, FieldType, PartialEq, Serialize)] +pub struct Tags(pub std::collections::HashSet); + +/// Blog metadata. +#[model] +#[derive(Debug, Default, Validate)] +pub struct Blog { + /// Id of the blog. + #[garde(skip)] + pub id: BlogId, + /// Name of the blog. + #[garde(dive)] + pub name: BlogName, +} +impl Blog { + /// Create a new Blog. + pub fn new(name: impl Into) -> Result { + let blog = Blog { + id: BlogId(uuid::Uuid::new_v4()), + name: BlogName(name.into()), + }; + blog.validate(&())?; + Ok(blog) + } +} +/// Post details, including a [ForeignKey] to [Blog] +/// and storing tags in [Tags] JSON field. +#[model] +pub struct Post { + /// Id of the blog post. + pub id: PostId, + /// Title of the blog post. + pub title: String, + /// Body of the blog post. + pub body: String, + /// Whether the blog post has been published. + pub published: bool, + /// Tags for the blog post. + pub tags: Tags, + /// The [Blog] this post is attached to. + pub blog: ForeignKey, + /// Byline of the post. + pub byline: Option, + /// How many likes this post has. + pub likes: i32, +} +impl Post { + /// Create a new Post. + pub fn new(blog: &Blog, title: String, body: String) -> Self { + Post { + id: PostId(uuid::Uuid::new_v4()), + title, + body, + published: false, + tags: Tags::default(), + blog: blog.into(), + byline: None, + likes: 0, + } + } +} diff --git a/examples/newtype/tests/rollback.rs b/examples/newtype/tests/rollback.rs new file mode 100644 index 00000000..dbe571c7 --- /dev/null +++ b/examples/newtype/tests/rollback.rs @@ -0,0 +1,61 @@ +use butane::db::{BackendConnection, Connection}; +use butane::migrations::{Migration, Migrations}; +use butane::DataObject; +use butane_test_helper::*; + +use newtype::models::{Blog, Post, Tags}; + +fn insert_data(connection: &Connection) { + if connection.backend_name() == "sqlite" { + // https://github.com/Electron100/butane/issues/226 + return; + } + let mut cats_blog = Blog::new("Cats").unwrap(); + cats_blog.save(connection).unwrap(); + + let mut post = Post::new( + &cats_blog, + "The Tiger".to_string(), + "The tiger is a cat which would very much like to eat you.".to_string(), + ); + post.published = true; + post.likes = 4; + post.tags = Tags(std::collections::HashSet::from([ + "asia".to_string(), + "danger".to_string(), + ])); + post.save(connection).unwrap(); +} + +fn migrate_and_rollback(mut connection: Connection) { + // Migrate forward. + let base_dir = std::path::PathBuf::from(".butane"); + let migrations = butane_cli::get_migrations(&base_dir).unwrap(); + let to_apply = migrations.unapplied_migrations(&connection).unwrap(); + for migration in &to_apply { + migration + .apply(&mut connection) + .unwrap_or_else(|err| panic!("migration {} failed: {err}", migration.name())); + eprintln!("Applied {}", migration.name()); + } + + insert_data(&connection); + + // Rollback migrations. + for migration in to_apply.iter().rev() { + if connection.backend_name() == "pg" && migration.name() == "20240401_095709389_init" { + // Postgres error db error: ERROR: cannot drop table blog because other objects depend on it + // DETAIL: constraint post_blog_fkey on table post depends on table blog + // HINT: Use DROP ... CASCADE to drop the dependent objects too. + let err = migration.downgrade(&mut connection).unwrap_err(); + eprintln!("Rolled back {} failed: {err:?}", migration.name()); + return; + } + + migration + .downgrade(&mut connection) + .unwrap_or_else(|err| panic!("rollback of {} failed: {err}", migration.name())); + eprintln!("Rolled back {}", migration.name()); + } +} +testall_no_migrate!(migrate_and_rollback); diff --git a/examples/newtype/tests/validation.rs b/examples/newtype/tests/validation.rs new file mode 100644 index 00000000..8cf9b8cf --- /dev/null +++ b/examples/newtype/tests/validation.rs @@ -0,0 +1,7 @@ +use newtype::models::Blog; + +#[test] +fn blog_name() { + Blog::new("Dog").unwrap(); + Blog::new("DogĀ£").unwrap_err(); +}