mirror of
				https://github.com/Phundrak/georm.git
				synced 2025-11-04 01:11:10 +00:00 
			
		
		
		
	feat(examples): add PostgreSQL example with user relationship
Adds an example demonstrating user, comment, and follower relationship including: - User management with profiles - Comments (not really useful, just for showcasing) - Follower/follozing relationships - Ineractive CLI interface with CRUD operations - Database migrations for the example schema
This commit is contained in:
		
							parent
							
								
									9e56952dc6
								
							
						
					
					
						commit
						190c4d7b1d
					
				@ -1,14 +0,0 @@
 | 
				
			|||||||
;;; Directory Local Variables            -*- no-byte-compile: t -*-
 | 
					 | 
				
			||||||
;;; For more information see (info "(emacs) Directory Variables")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
((rustic-mode . ((fill-column . 80)))
 | 
					 | 
				
			||||||
 (sql-mode . ((eval . (progn
 | 
					 | 
				
			||||||
                        (setq-local lsp-sqls-connections
 | 
					 | 
				
			||||||
                                    `(((driver . "postgresql")
 | 
					 | 
				
			||||||
                                       (dataSourceName \,
 | 
					 | 
				
			||||||
                                                       (format "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable"
 | 
					 | 
				
			||||||
                                                               (getenv "DB_HOST")
 | 
					 | 
				
			||||||
                                                               (getenv "DB_PORT")
 | 
					 | 
				
			||||||
                                                               (getenv "DB_USER")
 | 
					 | 
				
			||||||
                                                               (getenv "DB_PASSWORD")
 | 
					 | 
				
			||||||
                                                               (getenv "DB_NAME")))))))))))
 | 
					 | 
				
			||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,7 +1,8 @@
 | 
				
			|||||||
.direnv
 | 
					 | 
				
			||||||
.env
 | 
					.env
 | 
				
			||||||
/coverage
 | 
					/coverage
 | 
				
			||||||
/target
 | 
					/target
 | 
				
			||||||
 | 
					/.sqls
 | 
				
			||||||
 | 
					/examples/target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Devenv
 | 
					# Devenv
 | 
				
			||||||
.devenv*
 | 
					.devenv*
 | 
				
			||||||
@ -17,6 +18,7 @@ devenv.local.nix
 | 
				
			|||||||
*~
 | 
					*~
 | 
				
			||||||
\#*\#
 | 
					\#*\#
 | 
				
			||||||
.\#*
 | 
					.\#*
 | 
				
			||||||
 | 
					.dir-locals.el
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Vim files
 | 
					# Vim files
 | 
				
			||||||
*.swp
 | 
					*.swp
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										329
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										329
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -23,6 +23,56 @@ version = "0.2.21"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
 | 
					checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "anstream"
 | 
				
			||||||
 | 
					version = "0.6.19"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "anstyle",
 | 
				
			||||||
 | 
					 "anstyle-parse",
 | 
				
			||||||
 | 
					 "anstyle-query",
 | 
				
			||||||
 | 
					 "anstyle-wincon",
 | 
				
			||||||
 | 
					 "colorchoice",
 | 
				
			||||||
 | 
					 "is_terminal_polyfill",
 | 
				
			||||||
 | 
					 "utf8parse",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "anstyle"
 | 
				
			||||||
 | 
					version = "1.0.11"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "anstyle-parse"
 | 
				
			||||||
 | 
					version = "0.2.7"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "utf8parse",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "anstyle-query"
 | 
				
			||||||
 | 
					version = "1.1.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "windows-sys 0.59.0",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "anstyle-wincon"
 | 
				
			||||||
 | 
					version = "3.0.9"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "anstyle",
 | 
				
			||||||
 | 
					 "once_cell_polyfill",
 | 
				
			||||||
 | 
					 "windows-sys 0.59.0",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "arrayvec"
 | 
					name = "arrayvec"
 | 
				
			||||||
version = "0.7.6"
 | 
					version = "0.7.6"
 | 
				
			||||||
@ -71,6 +121,12 @@ version = "1.8.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
 | 
					checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "bitflags"
 | 
				
			||||||
 | 
					version = "1.3.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "bitflags"
 | 
					name = "bitflags"
 | 
				
			||||||
version = "2.9.1"
 | 
					version = "2.9.1"
 | 
				
			||||||
@ -107,6 +163,52 @@ version = "1.0.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
					checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap"
 | 
				
			||||||
 | 
					version = "4.5.39"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "clap_builder",
 | 
				
			||||||
 | 
					 "clap_derive",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap_builder"
 | 
				
			||||||
 | 
					version = "4.5.39"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "anstream",
 | 
				
			||||||
 | 
					 "anstyle",
 | 
				
			||||||
 | 
					 "clap_lex",
 | 
				
			||||||
 | 
					 "strsim 0.11.1",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap_derive"
 | 
				
			||||||
 | 
					version = "4.5.32"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "heck 0.5.0",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap_lex"
 | 
				
			||||||
 | 
					version = "0.7.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "colorchoice"
 | 
				
			||||||
 | 
					version = "1.0.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "concurrent-queue"
 | 
					name = "concurrent-queue"
 | 
				
			||||||
version = "2.5.0"
 | 
					version = "2.5.0"
 | 
				
			||||||
@ -161,6 +263,31 @@ version = "0.8.21"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 | 
					checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "crossterm"
 | 
				
			||||||
 | 
					version = "0.25.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "bitflags 1.3.2",
 | 
				
			||||||
 | 
					 "crossterm_winapi",
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "mio 0.8.11",
 | 
				
			||||||
 | 
					 "parking_lot",
 | 
				
			||||||
 | 
					 "signal-hook",
 | 
				
			||||||
 | 
					 "signal-hook-mio",
 | 
				
			||||||
 | 
					 "winapi",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "crossterm_winapi"
 | 
				
			||||||
 | 
					version = "0.9.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "winapi",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "crypto-common"
 | 
					name = "crypto-common"
 | 
				
			||||||
version = "0.1.6"
 | 
					version = "0.1.6"
 | 
				
			||||||
@ -193,7 +320,7 @@ dependencies = [
 | 
				
			|||||||
 "arrayvec",
 | 
					 "arrayvec",
 | 
				
			||||||
 "proc-macro2",
 | 
					 "proc-macro2",
 | 
				
			||||||
 "quote",
 | 
					 "quote",
 | 
				
			||||||
 "strsim",
 | 
					 "strsim 0.10.0",
 | 
				
			||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -252,6 +379,12 @@ version = "0.15.7"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 | 
					checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "dyn-clone"
 | 
				
			||||||
 | 
					version = "1.0.19"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "either"
 | 
					name = "either"
 | 
				
			||||||
version = "1.15.0"
 | 
					version = "1.15.0"
 | 
				
			||||||
@ -387,6 +520,24 @@ dependencies = [
 | 
				
			|||||||
 "slab",
 | 
					 "slab",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "fuzzy-matcher"
 | 
				
			||||||
 | 
					version = "0.3.7"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "thread_local",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "fxhash"
 | 
				
			||||||
 | 
					version = "0.2.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "byteorder",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "generic-array"
 | 
					name = "generic-array"
 | 
				
			||||||
version = "0.14.7"
 | 
					version = "0.14.7"
 | 
				
			||||||
@ -416,6 +567,18 @@ dependencies = [
 | 
				
			|||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "georm-users-comments-and-followers"
 | 
				
			||||||
 | 
					version = "0.1.1"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "clap",
 | 
				
			||||||
 | 
					 "georm",
 | 
				
			||||||
 | 
					 "inquire",
 | 
				
			||||||
 | 
					 "sqlx",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "tokio",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "getrandom"
 | 
					name = "getrandom"
 | 
				
			||||||
version = "0.2.16"
 | 
					version = "0.2.16"
 | 
				
			||||||
@ -447,9 +610,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hashbrown"
 | 
					name = "hashbrown"
 | 
				
			||||||
version = "0.15.3"
 | 
					version = "0.15.4"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
 | 
					checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "allocator-api2",
 | 
					 "allocator-api2",
 | 
				
			||||||
 "equivalent",
 | 
					 "equivalent",
 | 
				
			||||||
@ -633,6 +796,29 @@ dependencies = [
 | 
				
			|||||||
 "hashbrown",
 | 
					 "hashbrown",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "inquire"
 | 
				
			||||||
 | 
					version = "0.7.5"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "bitflags 2.9.1",
 | 
				
			||||||
 | 
					 "crossterm",
 | 
				
			||||||
 | 
					 "dyn-clone",
 | 
				
			||||||
 | 
					 "fuzzy-matcher",
 | 
				
			||||||
 | 
					 "fxhash",
 | 
				
			||||||
 | 
					 "newline-converter",
 | 
				
			||||||
 | 
					 "once_cell",
 | 
				
			||||||
 | 
					 "unicode-segmentation",
 | 
				
			||||||
 | 
					 "unicode-width",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "is_terminal_polyfill"
 | 
				
			||||||
 | 
					version = "1.70.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "itoa"
 | 
					name = "itoa"
 | 
				
			||||||
version = "1.0.15"
 | 
					version = "1.0.15"
 | 
				
			||||||
@ -717,6 +903,18 @@ dependencies = [
 | 
				
			|||||||
 "adler2",
 | 
					 "adler2",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "mio"
 | 
				
			||||||
 | 
					version = "0.8.11"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "wasi 0.11.0+wasi-snapshot-preview1",
 | 
				
			||||||
 | 
					 "windows-sys 0.48.0",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "mio"
 | 
					name = "mio"
 | 
				
			||||||
version = "1.0.4"
 | 
					version = "1.0.4"
 | 
				
			||||||
@ -728,6 +926,15 @@ dependencies = [
 | 
				
			|||||||
 "windows-sys 0.59.0",
 | 
					 "windows-sys 0.59.0",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "newline-converter"
 | 
				
			||||||
 | 
					version = "0.3.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unicode-segmentation",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "num-bigint-dig"
 | 
					name = "num-bigint-dig"
 | 
				
			||||||
version = "0.8.4"
 | 
					version = "0.8.4"
 | 
				
			||||||
@ -790,6 +997,12 @@ version = "1.21.3"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 | 
					checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "once_cell_polyfill"
 | 
				
			||||||
 | 
					version = "1.70.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "parking"
 | 
					name = "parking"
 | 
				
			||||||
version = "2.2.1"
 | 
					version = "2.2.1"
 | 
				
			||||||
@ -990,7 +1203,7 @@ version = "0.5.12"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
 | 
					checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "bitflags",
 | 
					 "bitflags 2.9.1",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -1097,6 +1310,36 @@ dependencies = [
 | 
				
			|||||||
 "digest",
 | 
					 "digest",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "signal-hook"
 | 
				
			||||||
 | 
					version = "0.3.18"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "signal-hook-registry",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "signal-hook-mio"
 | 
				
			||||||
 | 
					version = "0.2.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					 "mio 0.8.11",
 | 
				
			||||||
 | 
					 "signal-hook",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "signal-hook-registry"
 | 
				
			||||||
 | 
					version = "1.4.5"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libc",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "signature"
 | 
					name = "signature"
 | 
				
			||||||
version = "2.2.0"
 | 
					version = "2.2.0"
 | 
				
			||||||
@ -1245,7 +1488,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "atoi",
 | 
					 "atoi",
 | 
				
			||||||
 "base64",
 | 
					 "base64",
 | 
				
			||||||
 "bitflags",
 | 
					 "bitflags 2.9.1",
 | 
				
			||||||
 "byteorder",
 | 
					 "byteorder",
 | 
				
			||||||
 "bytes",
 | 
					 "bytes",
 | 
				
			||||||
 "crc",
 | 
					 "crc",
 | 
				
			||||||
@ -1286,7 +1529,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "atoi",
 | 
					 "atoi",
 | 
				
			||||||
 "base64",
 | 
					 "base64",
 | 
				
			||||||
 "bitflags",
 | 
					 "bitflags 2.9.1",
 | 
				
			||||||
 "byteorder",
 | 
					 "byteorder",
 | 
				
			||||||
 "crc",
 | 
					 "crc",
 | 
				
			||||||
 "dotenvy",
 | 
					 "dotenvy",
 | 
				
			||||||
@ -1361,6 +1604,12 @@ version = "0.10.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 | 
					checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "strsim"
 | 
				
			||||||
 | 
					version = "0.11.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "subtle"
 | 
					name = "subtle"
 | 
				
			||||||
version = "2.6.1"
 | 
					version = "2.6.1"
 | 
				
			||||||
@ -1409,6 +1658,16 @@ dependencies = [
 | 
				
			|||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "thread_local"
 | 
				
			||||||
 | 
					version = "1.1.8"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "cfg-if",
 | 
				
			||||||
 | 
					 "once_cell",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tinystr"
 | 
					name = "tinystr"
 | 
				
			||||||
version = "0.8.1"
 | 
					version = "0.8.1"
 | 
				
			||||||
@ -1443,12 +1702,26 @@ dependencies = [
 | 
				
			|||||||
 "backtrace",
 | 
					 "backtrace",
 | 
				
			||||||
 "bytes",
 | 
					 "bytes",
 | 
				
			||||||
 "libc",
 | 
					 "libc",
 | 
				
			||||||
 "mio",
 | 
					 "mio 1.0.4",
 | 
				
			||||||
 | 
					 "parking_lot",
 | 
				
			||||||
 "pin-project-lite",
 | 
					 "pin-project-lite",
 | 
				
			||||||
 | 
					 "signal-hook-registry",
 | 
				
			||||||
 "socket2",
 | 
					 "socket2",
 | 
				
			||||||
 | 
					 "tokio-macros",
 | 
				
			||||||
 "windows-sys 0.52.0",
 | 
					 "windows-sys 0.52.0",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "tokio-macros"
 | 
				
			||||||
 | 
					version = "2.5.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tokio-stream"
 | 
					name = "tokio-stream"
 | 
				
			||||||
version = "0.1.17"
 | 
					version = "0.1.17"
 | 
				
			||||||
@ -1542,6 +1815,18 @@ version = "0.1.3"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
 | 
					checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-segmentation"
 | 
				
			||||||
 | 
					version = "1.12.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-width"
 | 
				
			||||||
 | 
					version = "0.1.14"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "url"
 | 
					name = "url"
 | 
				
			||||||
version = "2.5.4"
 | 
					version = "2.5.4"
 | 
				
			||||||
@ -1559,6 +1844,12 @@ version = "1.0.4"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 | 
					checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "utf8parse"
 | 
				
			||||||
 | 
					version = "0.2.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "vcpkg"
 | 
					name = "vcpkg"
 | 
				
			||||||
version = "0.2.15"
 | 
					version = "0.2.15"
 | 
				
			||||||
@ -1602,6 +1893,28 @@ dependencies = [
 | 
				
			|||||||
 "wasite",
 | 
					 "wasite",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "winapi"
 | 
				
			||||||
 | 
					version = "0.3.9"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "winapi-i686-pc-windows-gnu",
 | 
				
			||||||
 | 
					 "winapi-x86_64-pc-windows-gnu",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "winapi-i686-pc-windows-gnu"
 | 
				
			||||||
 | 
					version = "0.4.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "winapi-x86_64-pc-windows-gnu"
 | 
				
			||||||
 | 
					version = "0.4.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "windows-sys"
 | 
					name = "windows-sys"
 | 
				
			||||||
version = "0.48.0"
 | 
					version = "0.48.0"
 | 
				
			||||||
@ -1765,7 +2078,7 @@ version = "0.39.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
 | 
					checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "bitflags",
 | 
					 "bitflags 2.9.1",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,9 @@
 | 
				
			|||||||
[workspace]
 | 
					[workspace]
 | 
				
			||||||
members = [".", "georm-macros"]
 | 
					members = [
 | 
				
			||||||
 | 
					    ".",
 | 
				
			||||||
 | 
					    "georm-macros",
 | 
				
			||||||
 | 
					    "examples/postgres/*"
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[workspace.package]
 | 
					[workspace.package]
 | 
				
			||||||
version = "0.1.1"
 | 
					version = "0.1.1"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							@ -491,6 +491,32 @@ Georm is designed for zero runtime overhead:
 | 
				
			|||||||
- **Minimal allocations**: Efficient use of owned vs borrowed data
 | 
					- **Minimal allocations**: Efficient use of owned vs borrowed data
 | 
				
			||||||
- **SQLx integration**: Leverages SQLx's optimized PostgreSQL driver
 | 
					- **SQLx integration**: Leverages SQLx's optimized PostgreSQL driver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Comprehensive Example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For an example showcasing user management, comments, and follower relationships, see the example in `examples/postgres/users-comments-and-followers/`. This example demonstrates:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- User management and profile management
 | 
				
			||||||
 | 
					- Comment system with user associations
 | 
				
			||||||
 | 
					- Follower/following relationships (many-to-many)
 | 
				
			||||||
 | 
					- Interactive CLI interface with CRUD operations
 | 
				
			||||||
 | 
					- Database migrations and schema setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To run the example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					# Set up your database
 | 
				
			||||||
 | 
					export DATABASE_URL="postgres://username:password@localhost/georm_example"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run migrations
 | 
				
			||||||
 | 
					cargo sqlx migrate run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run the example
 | 
				
			||||||
 | 
					cd examples/postgres/users-comments-and-followers
 | 
				
			||||||
 | 
					cargo run help # For a list of all available actions
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Comparison
 | 
					## Comparison
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Feature              | Georm | SeaORM | Diesel |
 | 
					| Feature              | Georm | SeaORM | Diesel |
 | 
				
			||||||
@ -509,6 +535,7 @@ Georm is designed for zero runtime overhead:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Medium Priority
 | 
					### Medium Priority
 | 
				
			||||||
- **Multi-Database Support**: MySQL and SQLite support with feature flags
 | 
					- **Multi-Database Support**: MySQL and SQLite support with feature flags
 | 
				
			||||||
 | 
					- **Field-Based Queries**: Generate `find_by_{field_name}` methods that return `Vec<T>` for regular fields or `Option<T>` for unique fields
 | 
				
			||||||
- **Relationship Optimization**: Eager loading and N+1 query prevention
 | 
					- **Relationship Optimization**: Eager loading and N+1 query prevention
 | 
				
			||||||
- **Composite Primary Keys**: Multi-field primary key support
 | 
					- **Composite Primary Keys**: Multi-field primary key support
 | 
				
			||||||
- **Soft Delete**: Optional soft delete with `deleted_at` timestamps
 | 
					- **Soft Delete**: Optional soft delete with `deleted_at` timestamps
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								examples/postgres/users-comments-and-followers/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								examples/postgres/users-comments-and-followers/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "georm-users-comments-and-followers"
 | 
				
			||||||
 | 
					workspace = "../../../"
 | 
				
			||||||
 | 
					publish = false
 | 
				
			||||||
 | 
					version.workspace = true
 | 
				
			||||||
 | 
					edition.workspace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					georm = { path = "../../.." }
 | 
				
			||||||
 | 
					sqlx = { workspace = true }
 | 
				
			||||||
 | 
					clap = { version = "4.4", features = ["derive"] }
 | 
				
			||||||
 | 
					inquire = "0.7.5"
 | 
				
			||||||
 | 
					thiserror = "2.0.11"
 | 
				
			||||||
 | 
					tokio = { version = "1.43.0", features = ["full"] }
 | 
				
			||||||
@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					use super::{Executable, Result};
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    errors::UserInputError,
 | 
				
			||||||
 | 
					    models::{Comment, CommentDefault, User},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use clap::{Args, Subcommand};
 | 
				
			||||||
 | 
					use georm::{Defaultable, Georm};
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Args, Clone)]
 | 
				
			||||||
 | 
					pub struct CommentArgs {
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    pub command: CommentCommand,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for CommentArgs {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        self.command.execute(pool).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Subcommand)]
 | 
				
			||||||
 | 
					pub enum CommentCommand {
 | 
				
			||||||
 | 
					    Create {
 | 
				
			||||||
 | 
					        text: Option<String>,
 | 
				
			||||||
 | 
					        username: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Remove {
 | 
				
			||||||
 | 
					        id: Option<i32>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    RemoveFromUser {
 | 
				
			||||||
 | 
					        username: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ListFromUser {
 | 
				
			||||||
 | 
					        username: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    List,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for CommentCommand {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            CommentCommand::Create { text, username } => {
 | 
				
			||||||
 | 
					                create_comment(username.clone(), text.clone(), pool).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            CommentCommand::Remove { id } => remove_comment(*id, pool).await,
 | 
				
			||||||
 | 
					            CommentCommand::RemoveFromUser { username } => {
 | 
				
			||||||
 | 
					                remove_user_comment(username.clone(), pool).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            CommentCommand::ListFromUser { username } => {
 | 
				
			||||||
 | 
					                list_user_comments(username.clone(), pool).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            CommentCommand::List => list_comments(pool).await,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn create_comment(
 | 
				
			||||||
 | 
					    username: Option<String>,
 | 
				
			||||||
 | 
					    text: Option<String>,
 | 
				
			||||||
 | 
					    pool: &sqlx::PgPool,
 | 
				
			||||||
 | 
					) -> Result {
 | 
				
			||||||
 | 
					    let prompt = "Who is creating the comment?";
 | 
				
			||||||
 | 
					    let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
 | 
				
			||||||
 | 
					    let content = match text {
 | 
				
			||||||
 | 
					        Some(text) => text,
 | 
				
			||||||
 | 
					        None => inquire::Text::new("Content of the comment:")
 | 
				
			||||||
 | 
					            .prompt()
 | 
				
			||||||
 | 
					            .map_err(UserInputError::InquireError)?,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let comment = CommentDefault {
 | 
				
			||||||
 | 
					        author_id: user.id,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        id: None,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let comment = comment.create(pool).await?;
 | 
				
			||||||
 | 
					    println!("Successfuly created comment:\n{comment}");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn remove_comment(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let prompt = "Select the comment to remove:";
 | 
				
			||||||
 | 
					    let comment = match id {
 | 
				
			||||||
 | 
					        Some(id) => Comment::find(pool, &id)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(UserInputError::DatabaseError)?
 | 
				
			||||||
 | 
					            .ok_or(UserInputError::CommentDoesNotExist)?,
 | 
				
			||||||
 | 
					        None => Comment::select_comment(prompt, pool).await?,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    comment.delete(pool).await?;
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn remove_user_comment(username: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let prompt = "Select user whose comment you want to delete:";
 | 
				
			||||||
 | 
					    let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
 | 
				
			||||||
 | 
					    let comments: HashMap<String, Comment> = user
 | 
				
			||||||
 | 
					        .get_comments(pool)
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .into_iter()
 | 
				
			||||||
 | 
					        .map(|comment| (comment.content.clone(), comment))
 | 
				
			||||||
 | 
					        .collect();
 | 
				
			||||||
 | 
					    let selected_comment_content =
 | 
				
			||||||
 | 
					        inquire::Select::new(prompt, comments.clone().into_keys().collect())
 | 
				
			||||||
 | 
					            .prompt()
 | 
				
			||||||
 | 
					            .map_err(UserInputError::InquireError)?;
 | 
				
			||||||
 | 
					    let comment: &Comment = comments.get(&selected_comment_content).unwrap();
 | 
				
			||||||
 | 
					    comment.delete(pool).await?;
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn list_user_comments(username: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let prompt = "User whose comment you want to list:";
 | 
				
			||||||
 | 
					    let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
 | 
				
			||||||
 | 
					    println!("List of comments from user:\n");
 | 
				
			||||||
 | 
					    for comment in user.get_comments(pool).await? {
 | 
				
			||||||
 | 
					        println!("{comment}\n");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn list_comments(pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let comments = Comment::find_all(pool).await?;
 | 
				
			||||||
 | 
					    println!("List of all comments:\n");
 | 
				
			||||||
 | 
					    for comment in comments {
 | 
				
			||||||
 | 
					        println!("{comment}\n")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,134 @@
 | 
				
			|||||||
 | 
					use super::{Executable, Result};
 | 
				
			||||||
 | 
					use crate::models::{FollowerDefault, User};
 | 
				
			||||||
 | 
					use clap::{Args, Subcommand};
 | 
				
			||||||
 | 
					use georm::Defaultable;
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Args, Clone)]
 | 
				
			||||||
 | 
					pub struct FollowersArgs {
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    pub command: FollowersCommand,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for FollowersArgs {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        self.command.execute(pool).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Subcommand)]
 | 
				
			||||||
 | 
					pub enum FollowersCommand {
 | 
				
			||||||
 | 
					    Follow {
 | 
				
			||||||
 | 
					        follower: Option<String>,
 | 
				
			||||||
 | 
					        followed: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Unfollow {
 | 
				
			||||||
 | 
					        follower: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ListFollowers {
 | 
				
			||||||
 | 
					        user: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ListFollowed {
 | 
				
			||||||
 | 
					        user: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for FollowersCommand {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            FollowersCommand::Follow { follower, followed } => {
 | 
				
			||||||
 | 
					                follow_user(follower.clone(), followed.clone(), pool).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            FollowersCommand::Unfollow { follower } => unfollow_user(follower.clone(), pool).await,
 | 
				
			||||||
 | 
					            FollowersCommand::ListFollowers { user } => {
 | 
				
			||||||
 | 
					                list_user_followers(user.clone(), pool).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            FollowersCommand::ListFollowed { user } => list_user_followed(user.clone(), pool).await,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn follow_user(
 | 
				
			||||||
 | 
					    follower: Option<String>,
 | 
				
			||||||
 | 
					    followed: Option<String>,
 | 
				
			||||||
 | 
					    pool: &sqlx::PgPool,
 | 
				
			||||||
 | 
					) -> Result {
 | 
				
			||||||
 | 
					    let follower = User::get_user_by_username_or_select(
 | 
				
			||||||
 | 
					        follower.as_deref(),
 | 
				
			||||||
 | 
					        "Select who will be following someone:",
 | 
				
			||||||
 | 
					        pool,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    let followed = User::get_user_by_username_or_select(
 | 
				
			||||||
 | 
					        followed.as_deref(),
 | 
				
			||||||
 | 
					        "Select who will be followed:",
 | 
				
			||||||
 | 
					        pool,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    let follow = FollowerDefault {
 | 
				
			||||||
 | 
					        id: None,
 | 
				
			||||||
 | 
					        follower: follower.id,
 | 
				
			||||||
 | 
					        followed: followed.id,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    follow.create(pool).await?;
 | 
				
			||||||
 | 
					    println!("User {follower} now follows {followed}");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn unfollow_user(follower: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let follower =
 | 
				
			||||||
 | 
					        User::get_user_by_username_or_select(follower.as_deref(), "Select who is following", pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					    let followed_list: HashMap<String, User> = follower
 | 
				
			||||||
 | 
					        .get_followed(pool)
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|person| (person.username.clone(), person.clone()))
 | 
				
			||||||
 | 
					        .collect();
 | 
				
			||||||
 | 
					    let followed = inquire::Select::new(
 | 
				
			||||||
 | 
					        "Who to unfollow?",
 | 
				
			||||||
 | 
					        followed_list.clone().into_keys().collect(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .prompt()
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					    let followed = followed_list.get(&followed).unwrap();
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "DELETE FROM Followers WHERE follower = $1 AND followed = $2",
 | 
				
			||||||
 | 
					        follower.id,
 | 
				
			||||||
 | 
					        followed.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(pool)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    println!("User {follower} unfollowed {followed}");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn list_user_followers(user: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let user = User::get_user_by_username_or_select(
 | 
				
			||||||
 | 
					        user.as_deref(),
 | 
				
			||||||
 | 
					        "Whose followers do you want to display?",
 | 
				
			||||||
 | 
					        pool,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    println!("List of followers of {user}:\n");
 | 
				
			||||||
 | 
					    user.get_followers(pool)
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .for_each(|person| println!("{person}"));
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn list_user_followed(user: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let user = User::get_user_by_username_or_select(
 | 
				
			||||||
 | 
					        user.as_deref(),
 | 
				
			||||||
 | 
					        "Whose follows do you want to display?",
 | 
				
			||||||
 | 
					        pool,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    println!("List of people followed by {user}:\n");
 | 
				
			||||||
 | 
					    user.get_followed(pool)
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .for_each(|person| println!("{person}"));
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					use clap::{Parser, Subcommand};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod comments;
 | 
				
			||||||
 | 
					mod followers;
 | 
				
			||||||
 | 
					mod users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Result = crate::Result<()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait Executable {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Parser)]
 | 
				
			||||||
 | 
					pub struct Cli {
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    pub command: Commands,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for Cli {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        self.command.execute(pool).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Subcommand)]
 | 
				
			||||||
 | 
					pub enum Commands {
 | 
				
			||||||
 | 
					    Users(users::UserArgs),
 | 
				
			||||||
 | 
					    Followers(followers::FollowersArgs),
 | 
				
			||||||
 | 
					    Comments(comments::CommentArgs),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for Commands {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Commands::Users(user_args) => user_args.execute(pool).await,
 | 
				
			||||||
 | 
					            Commands::Followers(followers_args) => followers_args.execute(pool).await,
 | 
				
			||||||
 | 
					            Commands::Comments(comment_args) => comment_args.execute(pool).await,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								examples/postgres/users-comments-and-followers/src/cli/users.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								examples/postgres/users-comments-and-followers/src/cli/users.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					use super::{Executable, Result};
 | 
				
			||||||
 | 
					use crate::{errors::UserInputError, models::User};
 | 
				
			||||||
 | 
					use clap::{Args, Subcommand};
 | 
				
			||||||
 | 
					use georm::Georm;
 | 
				
			||||||
 | 
					use inquire::{max_length, min_length, required};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Args, Clone)]
 | 
				
			||||||
 | 
					pub struct UserArgs {
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    pub command: UserCommand,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for UserArgs {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        self.command.execute(pool).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Subcommand)]
 | 
				
			||||||
 | 
					pub enum UserCommand {
 | 
				
			||||||
 | 
					    Add { username: Option<String> },
 | 
				
			||||||
 | 
					    Remove { id: Option<i32> },
 | 
				
			||||||
 | 
					    UpdateProfile { id: Option<i32> },
 | 
				
			||||||
 | 
					    List,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Executable for UserCommand {
 | 
				
			||||||
 | 
					    async fn execute(&self, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            UserCommand::Add { username } => add_user(username.clone(), pool).await,
 | 
				
			||||||
 | 
					            UserCommand::Remove { id } => remove_user(*id, pool).await,
 | 
				
			||||||
 | 
					            UserCommand::UpdateProfile { id } => update_profile(*id, pool).await,
 | 
				
			||||||
 | 
					            UserCommand::List => list_all(pool).await,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn add_user(username: Option<String>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let username = match username {
 | 
				
			||||||
 | 
					        Some(username) => username,
 | 
				
			||||||
 | 
					        None => inquire::Text::new("Enter a username:")
 | 
				
			||||||
 | 
					            .prompt()
 | 
				
			||||||
 | 
					            .map_err(|_| UserInputError::InputRequired)?,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let user = User::try_new(&username, pool).await?;
 | 
				
			||||||
 | 
					    println!("The user {user} has been created!");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn remove_user(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let user = User::remove_interactive(id, pool).await?;
 | 
				
			||||||
 | 
					    println!("Removed user {user} from database");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let (user, mut profile) = User::update_profile(id, pool).await?;
 | 
				
			||||||
 | 
					    let update_display_name = inquire::Confirm::new(
 | 
				
			||||||
 | 
					        format!(
 | 
				
			||||||
 | 
					            "Your current display name is \"{}\", do you want to update it?",
 | 
				
			||||||
 | 
					            profile.get_display_name()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .as_str(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .with_default(false)
 | 
				
			||||||
 | 
					    .prompt()
 | 
				
			||||||
 | 
					    .map_err(UserInputError::InquireError)?;
 | 
				
			||||||
 | 
					    let display_name = if update_display_name {
 | 
				
			||||||
 | 
					        Some(
 | 
				
			||||||
 | 
					            inquire::Text::new("New display name:")
 | 
				
			||||||
 | 
					                .with_help_message("Your display name should not exceed 100 characters")
 | 
				
			||||||
 | 
					                .with_validator(min_length!(3))
 | 
				
			||||||
 | 
					                .with_validator(max_length!(100))
 | 
				
			||||||
 | 
					                .with_validator(required!())
 | 
				
			||||||
 | 
					                .prompt()
 | 
				
			||||||
 | 
					                .map_err(UserInputError::InquireError)?,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Some(profile.get_display_name())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let update_bio = inquire::Confirm::new(
 | 
				
			||||||
 | 
					        format!(
 | 
				
			||||||
 | 
					            "Your current bio is:\n===\n{}\n===\nDo you want to update it?",
 | 
				
			||||||
 | 
					            profile.get_bio()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .as_str(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .with_default(false)
 | 
				
			||||||
 | 
					    .prompt()
 | 
				
			||||||
 | 
					    .map_err(UserInputError::InquireError)?;
 | 
				
			||||||
 | 
					    let bio = if update_bio {
 | 
				
			||||||
 | 
					        Some(
 | 
				
			||||||
 | 
					            inquire::Text::new("New bio:")
 | 
				
			||||||
 | 
					                .with_validator(min_length!(0))
 | 
				
			||||||
 | 
					                .prompt()
 | 
				
			||||||
 | 
					                .map_err(UserInputError::InquireError)?,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Some(profile.get_bio())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let profile = profile.update_interactive(display_name, bio, pool).await?;
 | 
				
			||||||
 | 
					    println!("Profile of {user} updated:\n{profile}");
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn list_all(pool: &sqlx::PgPool) -> Result {
 | 
				
			||||||
 | 
					    let users = User::find_all(pool).await?;
 | 
				
			||||||
 | 
					    println!("List of users:\n");
 | 
				
			||||||
 | 
					    for user in users {
 | 
				
			||||||
 | 
					        println!("{user}");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								examples/postgres/users-comments-and-followers/src/errors.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								examples/postgres/users-comments-and-followers/src/errors.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Error)]
 | 
				
			||||||
 | 
					pub enum UserInputError {
 | 
				
			||||||
 | 
					    #[error("Input required")]
 | 
				
			||||||
 | 
					    InputRequired,
 | 
				
			||||||
 | 
					    #[error("User ID does not exist")]
 | 
				
			||||||
 | 
					    UserDoesNotExist,
 | 
				
			||||||
 | 
					    #[error("Comment does not exist")]
 | 
				
			||||||
 | 
					    CommentDoesNotExist,
 | 
				
			||||||
 | 
					    #[error("Unexpected error, please try again")]
 | 
				
			||||||
 | 
					    InquireError(#[from] inquire::error::InquireError),
 | 
				
			||||||
 | 
					    #[error("Error from database: {0}")]
 | 
				
			||||||
 | 
					    DatabaseError(#[from] sqlx::Error),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								examples/postgres/users-comments-and-followers/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								examples/postgres/users-comments-and-followers/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					mod cli;
 | 
				
			||||||
 | 
					mod errors;
 | 
				
			||||||
 | 
					mod models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					use cli::{Cli, Executable};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Result<T> = std::result::Result<T, errors::UserInputError>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::main]
 | 
				
			||||||
 | 
					async fn main() {
 | 
				
			||||||
 | 
					    let args = Cli::parse();
 | 
				
			||||||
 | 
					    let url = std::env::var("DATABASE_URL").expect("Environment variable DATABASE_URL must be set");
 | 
				
			||||||
 | 
					    let pool =
 | 
				
			||||||
 | 
					        sqlx::PgPool::connect_lazy(url.as_str()).expect("Failed to create database connection");
 | 
				
			||||||
 | 
					    match args.command.execute(&pool).await {
 | 
				
			||||||
 | 
					        Ok(_) => {}
 | 
				
			||||||
 | 
					        Err(e) => eprintln!("Error: {e}"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					use super::User;
 | 
				
			||||||
 | 
					use crate::{Result, errors::UserInputError};
 | 
				
			||||||
 | 
					use georm::Georm;
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Georm, Clone)]
 | 
				
			||||||
 | 
					#[georm(table = "Comments")]
 | 
				
			||||||
 | 
					pub struct Comment {
 | 
				
			||||||
 | 
					    #[georm(id, defaultable)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    #[georm(relation = {
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        name = "author"
 | 
				
			||||||
 | 
					    })]
 | 
				
			||||||
 | 
					    pub author_id: i32,
 | 
				
			||||||
 | 
					    pub content: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Comment {
 | 
				
			||||||
 | 
					    pub async fn select_comment(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> {
 | 
				
			||||||
 | 
					        let comments: HashMap<String, Self> = Self::find_all(pool)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					            .into_iter()
 | 
				
			||||||
 | 
					            .map(|comment| (comment.content.clone(), comment))
 | 
				
			||||||
 | 
					            .collect();
 | 
				
			||||||
 | 
					        let comment_content = inquire::Select::new(prompt, comments.clone().into_keys().collect())
 | 
				
			||||||
 | 
					            .prompt()
 | 
				
			||||||
 | 
					            .map_err(UserInputError::InquireError)?;
 | 
				
			||||||
 | 
					        let comment: &Self = comments.get(&comment_content).unwrap();
 | 
				
			||||||
 | 
					        Ok(comment.clone())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::fmt::Display for Comment {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        write!(
 | 
				
			||||||
 | 
					            f,
 | 
				
			||||||
 | 
					            "Comment:\nID:\t{}\nAuthor:\t{}\nContent:\t{}",
 | 
				
			||||||
 | 
					            self.id, self.author_id, self.content
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					use super::User;
 | 
				
			||||||
 | 
					use georm::Georm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Georm)]
 | 
				
			||||||
 | 
					#[georm(table = "Followers")]
 | 
				
			||||||
 | 
					pub struct Follower {
 | 
				
			||||||
 | 
					    #[georm(id, defaultable)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    #[georm(relation = {
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        name = "followed"
 | 
				
			||||||
 | 
					    })]
 | 
				
			||||||
 | 
					    pub followed: i32,
 | 
				
			||||||
 | 
					    #[georm(relation = {
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        name = "follower"
 | 
				
			||||||
 | 
					    })]
 | 
				
			||||||
 | 
					    pub follower: i32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					mod users;
 | 
				
			||||||
 | 
					pub use users::*;
 | 
				
			||||||
 | 
					mod profiles;
 | 
				
			||||||
 | 
					pub use profiles::*;
 | 
				
			||||||
 | 
					mod comments;
 | 
				
			||||||
 | 
					pub use comments::*;
 | 
				
			||||||
 | 
					mod followers;
 | 
				
			||||||
 | 
					pub use followers::*;
 | 
				
			||||||
@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					use super::User;
 | 
				
			||||||
 | 
					use crate::{Result, errors::UserInputError};
 | 
				
			||||||
 | 
					use georm::{Defaultable, Georm};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Georm, Default)]
 | 
				
			||||||
 | 
					#[georm(table = "Profiles")]
 | 
				
			||||||
 | 
					pub struct Profile {
 | 
				
			||||||
 | 
					    #[georm(id, defaultable)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    #[georm(relation = {
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        name = "user",
 | 
				
			||||||
 | 
					        nullable = false
 | 
				
			||||||
 | 
					    })]
 | 
				
			||||||
 | 
					    pub user_id: i32,
 | 
				
			||||||
 | 
					    pub bio: Option<String>,
 | 
				
			||||||
 | 
					    pub display_name: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::fmt::Display for Profile {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        write!(
 | 
				
			||||||
 | 
					            f,
 | 
				
			||||||
 | 
					            "Display Name:\t{}\nBiography:\n{}\n",
 | 
				
			||||||
 | 
					            self.get_display_name(),
 | 
				
			||||||
 | 
					            self.get_bio()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Profile {
 | 
				
			||||||
 | 
					    pub fn get_display_name(&self) -> String {
 | 
				
			||||||
 | 
					        self.display_name.clone().unwrap_or_default()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_bio(&self) -> String {
 | 
				
			||||||
 | 
					        self.bio.clone().unwrap_or_default()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn try_new(user_id: i32, pool: &sqlx::PgPool) -> Result<Self> {
 | 
				
			||||||
 | 
					        let profile = ProfileDefault {
 | 
				
			||||||
 | 
					            user_id,
 | 
				
			||||||
 | 
					            id: None,
 | 
				
			||||||
 | 
					            bio: None,
 | 
				
			||||||
 | 
					            display_name: None,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        profile
 | 
				
			||||||
 | 
					            .create(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(UserInputError::DatabaseError)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn update_interactive(
 | 
				
			||||||
 | 
					        &mut self,
 | 
				
			||||||
 | 
					        display_name: Option<String>,
 | 
				
			||||||
 | 
					        bio: Option<String>,
 | 
				
			||||||
 | 
					        pool: &sqlx::PgPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self> {
 | 
				
			||||||
 | 
					        self.display_name = display_name;
 | 
				
			||||||
 | 
					        self.bio = bio;
 | 
				
			||||||
 | 
					        self.update(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(UserInputError::DatabaseError)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{Result, errors::UserInputError};
 | 
				
			||||||
 | 
					use georm::{Defaultable, Georm};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{Comment, Profile};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Georm, Clone)]
 | 
				
			||||||
 | 
					#[georm(
 | 
				
			||||||
 | 
					    table = "Users",
 | 
				
			||||||
 | 
					    one_to_one = [{
 | 
				
			||||||
 | 
					        name = "profile", remote_id = "user_id", table = "Profiles", entity = Profile
 | 
				
			||||||
 | 
					    }],
 | 
				
			||||||
 | 
					    one_to_many = [{
 | 
				
			||||||
 | 
					        name = "comments", remote_id = "author_id", table = "Comments", entity = Comment
 | 
				
			||||||
 | 
					    }],
 | 
				
			||||||
 | 
					    many_to_many = [{
 | 
				
			||||||
 | 
					        name = "followers",
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        link = { table = "Followers", from = "followed", to = "follower" }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					        name = "followed",
 | 
				
			||||||
 | 
					        table = "Users",
 | 
				
			||||||
 | 
					        entity = User,
 | 
				
			||||||
 | 
					        link = { table = "Followers", from = "follower", to = "followed" }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub struct User {
 | 
				
			||||||
 | 
					    #[georm(id, defaultable)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    pub username: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::fmt::Display for User {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        write!(f, "{} (ID: {})", self.username, self.id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<&str> for UserDefault {
 | 
				
			||||||
 | 
					    fn from(value: &str) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            id: None,
 | 
				
			||||||
 | 
					            username: value.to_string(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl User {
 | 
				
			||||||
 | 
					    async fn select_user(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> {
 | 
				
			||||||
 | 
					        let users: HashMap<String, Self> = Self::find_all(pool)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					            .into_iter()
 | 
				
			||||||
 | 
					            .map(|user| (user.username.clone(), user))
 | 
				
			||||||
 | 
					            .collect();
 | 
				
			||||||
 | 
					        let username = inquire::Select::new(prompt, users.clone().into_keys().collect())
 | 
				
			||||||
 | 
					            .prompt()
 | 
				
			||||||
 | 
					            .map_err(UserInputError::InquireError)?;
 | 
				
			||||||
 | 
					        let user: &Self = users.get(&username).unwrap();
 | 
				
			||||||
 | 
					        Ok(user.clone())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_user_by_id_or_select(
 | 
				
			||||||
 | 
					        id: Option<i32>,
 | 
				
			||||||
 | 
					        prompt: &str,
 | 
				
			||||||
 | 
					        pool: &sqlx::PgPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self> {
 | 
				
			||||||
 | 
					        let user = match id {
 | 
				
			||||||
 | 
					            Some(id) => Self::find(pool, &id)
 | 
				
			||||||
 | 
					                .await?
 | 
				
			||||||
 | 
					                .ok_or(UserInputError::UserDoesNotExist)?,
 | 
				
			||||||
 | 
					            None => Self::select_user(prompt, pool).await?,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Ok(user)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_user_by_username_or_select(
 | 
				
			||||||
 | 
					        username: Option<&str>,
 | 
				
			||||||
 | 
					        prompt: &str,
 | 
				
			||||||
 | 
					        pool: &sqlx::PgPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self> {
 | 
				
			||||||
 | 
					        let user = match username {
 | 
				
			||||||
 | 
					            Some(username) => Self::find_by_username(username, pool)
 | 
				
			||||||
 | 
					                .await?
 | 
				
			||||||
 | 
					                .ok_or(UserInputError::UserDoesNotExist)?,
 | 
				
			||||||
 | 
					            None => Self::select_user(prompt, pool).await?,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Ok(user)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn find_by_username(username: &str, pool: &sqlx::PgPool) -> Result<Option<Self>> {
 | 
				
			||||||
 | 
					        sqlx::query_as!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "SELECT * FROM Users u WHERE u.username = $1",
 | 
				
			||||||
 | 
					            username
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_optional(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_err(UserInputError::DatabaseError)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn try_new(username: &str, pool: &sqlx::PgPool) -> Result<Self> {
 | 
				
			||||||
 | 
					        let user = UserDefault::from(username);
 | 
				
			||||||
 | 
					        user.create(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(UserInputError::DatabaseError)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn remove_interactive(id: Option<i32>, pool: &sqlx::PgPool) -> Result<Self> {
 | 
				
			||||||
 | 
					        let prompt = "Select a user to delete:";
 | 
				
			||||||
 | 
					        let user = Self::get_user_by_id_or_select(id, prompt, pool).await?;
 | 
				
			||||||
 | 
					        let _ = user.clone().delete(pool).await?;
 | 
				
			||||||
 | 
					        Ok(user)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result<(User, Profile)> {
 | 
				
			||||||
 | 
					        let prompt = "Select the user whose profile you want to update";
 | 
				
			||||||
 | 
					        let user = Self::get_user_by_id_or_select(id, prompt, pool).await?;
 | 
				
			||||||
 | 
					        let profile = match user.get_profile(pool).await? {
 | 
				
			||||||
 | 
					            Some(profile) => profile,
 | 
				
			||||||
 | 
					            None => Profile::try_new(user.id, pool).await?,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Ok((user, profile))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -50,8 +50,12 @@ fn generate_defaultable_trait_impl(
 | 
				
			|||||||
    let defaultable_fields: Vec<_> = fields.iter().filter(|f| f.defaultable).collect();
 | 
					    let defaultable_fields: Vec<_> = fields.iter().filter(|f| f.defaultable).collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Build static parts for non-defaultable fields
 | 
					    // Build static parts for non-defaultable fields
 | 
				
			||||||
    let static_field_names: Vec<String> = non_defaultable_fields.iter().map(|f| f.ident.to_string()).collect();
 | 
					    let static_field_names: Vec<String> = non_defaultable_fields
 | 
				
			||||||
    let static_field_idents: Vec<&syn::Ident> = non_defaultable_fields.iter().map(|f| &f.ident).collect();
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|f| f.ident.to_string())
 | 
				
			||||||
 | 
					        .collect();
 | 
				
			||||||
 | 
					    let static_field_idents: Vec<&syn::Ident> =
 | 
				
			||||||
 | 
					        non_defaultable_fields.iter().map(|f| &f.ident).collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Generate field checks for defaultable fields
 | 
					    // Generate field checks for defaultable fields
 | 
				
			||||||
    let mut field_checks = Vec::new();
 | 
					    let mut field_checks = Vec::new();
 | 
				
			||||||
 | 
				
			|||||||
@ -35,10 +35,13 @@ fn extract_georm_field_attrs(
 | 
				
			|||||||
        _ => {
 | 
					        _ => {
 | 
				
			||||||
            let id1 = identifiers.first().unwrap();
 | 
					            let id1 = identifiers.first().unwrap();
 | 
				
			||||||
            let id2 = identifiers.get(1).unwrap();
 | 
					            let id2 = identifiers.get(1).unwrap();
 | 
				
			||||||
            Err(syn::Error::new_spanned(id2.field.clone(), format!(
 | 
					            Err(syn::Error::new_spanned(
 | 
				
			||||||
 | 
					                id2.field.clone(),
 | 
				
			||||||
 | 
					                format!(
 | 
				
			||||||
                    "Field {} cannot be an identifier, {} already is one.\nOnly one identifier is supported.",
 | 
					                    "Field {} cannot be an identifier, {} already is one.\nOnly one identifier is supported.",
 | 
				
			||||||
                    id1.ident, id2.ident
 | 
					                    id1.ident, id2.ident
 | 
				
			||||||
            )))
 | 
					                ),
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					DROP TABLE IF EXISTS Followers;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS Comments;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS Profiles;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS Users;
 | 
				
			||||||
@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					-- Add migration script here
 | 
				
			||||||
 | 
					CREATE TABLE Users (
 | 
				
			||||||
 | 
					  id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					  username VARCHAR(100) UNIQUE NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE Profiles (
 | 
				
			||||||
 | 
					  id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					  user_id INT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					  bio TEXT,
 | 
				
			||||||
 | 
					  display_name VARCHAR(100),
 | 
				
			||||||
 | 
					  FOREIGN KEY (user_id) REFERENCES Users(id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE Comments (
 | 
				
			||||||
 | 
					  id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					  author_id INT NOT NULL,
 | 
				
			||||||
 | 
					  content TEXT NOT NULL,
 | 
				
			||||||
 | 
					  FOREIGN KEY (author_id) REFERENCES Users(id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE Followers (
 | 
				
			||||||
 | 
					  id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					  followed INT NOT NULL,
 | 
				
			||||||
 | 
					  follower INT NOT NULL,
 | 
				
			||||||
 | 
					  FOREIGN KEY (followed) REFERENCES Users(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					  FOREIGN KEY (follower) REFERENCES Users(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					  CHECK (followed != follower),
 | 
				
			||||||
 | 
					  UNIQUE (followed, follower)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@ -169,7 +169,7 @@ mod defaultable_tests {
 | 
				
			|||||||
            Ok(created) => {
 | 
					            Ok(created) => {
 | 
				
			||||||
                assert!(created.id > 0);
 | 
					                assert!(created.id > 0);
 | 
				
			||||||
                // If successful, name should have some default value
 | 
					                // If successful, name should have some default value
 | 
				
			||||||
            },
 | 
					            }
 | 
				
			||||||
            Err(e) => {
 | 
					            Err(e) => {
 | 
				
			||||||
                // Expected if no database default for name column
 | 
					                // Expected if no database default for name column
 | 
				
			||||||
                assert!(e.to_string().contains("null") || e.to_string().contains("NOT NULL"));
 | 
					                assert!(e.to_string().contains("null") || e.to_string().contains("NOT NULL"));
 | 
				
			||||||
@ -233,7 +233,11 @@ mod defaultable_tests {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let error = result2.unwrap_err();
 | 
					        let error = result2.unwrap_err();
 | 
				
			||||||
        let error_str = error.to_string();
 | 
					        let error_str = error.to_string();
 | 
				
			||||||
        assert!(error_str.contains("duplicate") || error_str.contains("unique") || error_str.contains("UNIQUE"));
 | 
					        assert!(
 | 
				
			||||||
 | 
					            error_str.contains("duplicate")
 | 
				
			||||||
 | 
					                || error_str.contains("unique")
 | 
				
			||||||
 | 
					                || error_str.contains("UNIQUE")
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[sqlx::test(fixtures("../tests/fixtures/simple_struct.sql"))]
 | 
					    #[sqlx::test(fixtures("../tests/fixtures/simple_struct.sql"))]
 | 
				
			||||||
@ -254,11 +258,15 @@ mod defaultable_tests {
 | 
				
			|||||||
                // No foreign key constraint - this is valid behavior
 | 
					                // No foreign key constraint - this is valid behavior
 | 
				
			||||||
                assert!(created.id > 0);
 | 
					                assert!(created.id > 0);
 | 
				
			||||||
                assert_eq!(created.biography_id, Some(99999));
 | 
					                assert_eq!(created.biography_id, Some(99999));
 | 
				
			||||||
            },
 | 
					            }
 | 
				
			||||||
            Err(e) => {
 | 
					            Err(e) => {
 | 
				
			||||||
                // Foreign key constraint violation
 | 
					                // Foreign key constraint violation
 | 
				
			||||||
                let error_str = e.to_string();
 | 
					                let error_str = e.to_string();
 | 
				
			||||||
                assert!(error_str.contains("foreign") || error_str.contains("constraint") || error_str.contains("violates"));
 | 
					                assert!(
 | 
				
			||||||
 | 
					                    error_str.contains("foreign")
 | 
				
			||||||
 | 
					                        || error_str.contains("constraint")
 | 
				
			||||||
 | 
					                        || error_str.contains("violates")
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -281,7 +289,7 @@ mod defaultable_tests {
 | 
				
			|||||||
            Ok(created) => {
 | 
					            Ok(created) => {
 | 
				
			||||||
                assert!(created.id > 0);
 | 
					                assert!(created.id > 0);
 | 
				
			||||||
                assert_eq!(created.name.len(), 10000);
 | 
					                assert_eq!(created.name.len(), 10000);
 | 
				
			||||||
            },
 | 
					            }
 | 
				
			||||||
            Err(e) => {
 | 
					            Err(e) => {
 | 
				
			||||||
                // Some kind of database limit hit
 | 
					                // Some kind of database limit hit
 | 
				
			||||||
                assert!(!e.to_string().is_empty());
 | 
					                assert!(!e.to_string().is_empty());
 | 
				
			||||||
@ -292,7 +300,6 @@ mod defaultable_tests {
 | 
				
			|||||||
    mod sql_validation_tests {
 | 
					    mod sql_validation_tests {
 | 
				
			||||||
        use super::*;
 | 
					        use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[sqlx::test(fixtures("../tests/fixtures/simple_struct.sql"))]
 | 
					        #[sqlx::test(fixtures("../tests/fixtures/simple_struct.sql"))]
 | 
				
			||||||
        async fn test_sql_generation_no_defaultable_fields(pool: PgPool) {
 | 
					        async fn test_sql_generation_no_defaultable_fields(pool: PgPool) {
 | 
				
			||||||
            // Test SQL generation when no defaultable fields have None values
 | 
					            // Test SQL generation when no defaultable fields have None values
 | 
				
			||||||
@ -401,7 +408,7 @@ mod defaultable_tests {
 | 
				
			|||||||
                Ok(created1) => {
 | 
					                Ok(created1) => {
 | 
				
			||||||
                    assert!(created1.id > 0);
 | 
					                    assert!(created1.id > 0);
 | 
				
			||||||
                    assert_eq!(created1.biography_id, Some(1));
 | 
					                    assert_eq!(created1.biography_id, Some(1));
 | 
				
			||||||
                },
 | 
					                }
 | 
				
			||||||
                Err(_) => {
 | 
					                Err(_) => {
 | 
				
			||||||
                    // Expected if name field has no database default
 | 
					                    // Expected if name field has no database default
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
@ -60,7 +60,10 @@ async fn create_fails_if_already_exists(pool: sqlx::PgPool) -> sqlx::Result<()>
 | 
				
			|||||||
    let result = author.create(&pool).await;
 | 
					    let result = author.create(&pool).await;
 | 
				
			||||||
    assert!(result.is_err());
 | 
					    assert!(result.is_err());
 | 
				
			||||||
    let error = result.err().unwrap();
 | 
					    let error = result.err().unwrap();
 | 
				
			||||||
    assert_eq!("error returned from database: duplicate key value violates unique constraint \"authors_pkey\"", error.to_string());
 | 
					    assert_eq!(
 | 
				
			||||||
 | 
					        "error returned from database: duplicate key value violates unique constraint \"authors_pkey\"",
 | 
				
			||||||
 | 
					        error.to_string()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user