1use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::str::FromStr;
12
13use smallvec::SmallVec;
14
15use crate::error::{Error, Result};
16
17pub(crate) const PG_IDENTIFIER_LIMIT: usize = 63;
19
20pub fn escape_name(name: &str) -> Result<String> {
49 let len = name.chars().count();
50 if len > PG_IDENTIFIER_LIMIT {
51 return Err(Error::InvalidName(format!(
52 "Name exceeds PostgreSQL identifier limit ({len} > {PG_IDENTIFIER_LIMIT})"
53 )));
54 }
55
56 let escaped_inner = name.replace('"', "\"\"");
57 Ok(format!("\"{escaped_inner}\""))
58}
59
60#[must_use]
80pub fn escape_sql_path(path: &str) -> String {
81 let escaped_inner = path.replace('"', "\"\"");
82 format!("\"{escaped_inner}\"")
83}
84
85#[must_use]
102pub fn escape_string_literal(value: &str) -> String {
103 format!("'{}'", value.replace('\'', "''"))
104}
105
106#[derive(Clone, Debug)]
122#[must_use = "Name represents a validated SQL identifier that should not be discarded. Use it in your SQL queries or table definitions"]
123pub struct Name {
124 escaped: String,
126 unescaped: String,
128}
129
130impl Name {
131 pub fn try_new(name: impl Into<String>) -> Result<Self> {
147 let unescaped = name.into();
148 if unescaped.is_empty() {
149 return Err(Error::InvalidName("Name must not be empty".into()));
150 }
151 let escaped = escape_name(&unescaped)?;
153 Ok(Name { escaped, unescaped })
154 }
155
156 #[must_use]
160 pub fn as_str(&self) -> &str {
161 &self.escaped
162 }
163
164 #[must_use]
169 pub fn unescaped(&self) -> &str {
170 &self.unescaped
171 }
172}
173
174fn parse_qualified_identifier(s: &str) -> SmallVec<[String; 3]> {
179 let mut parts = SmallVec::new();
180 let mut current = String::new();
181 let mut in_quotes = false;
182 let mut chars = s.chars().peekable();
183
184 while let Some(c) = chars.next() {
185 match c {
186 '"' => {
188 if in_quotes && chars.peek() == Some(&'"') {
190 current.push('"');
191 chars.next(); } else {
193 in_quotes = !in_quotes;
194 }
196 }
197 '.' if !in_quotes => {
199 if !current.is_empty() {
200 parts.push(current.split_off(0));
201 }
202 }
203 _ => current.push(c),
204 }
205 }
206 if !current.is_empty() {
207 parts.push(current);
208 }
209 parts
210}
211
212impl fmt::Display for Name {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "{}", self.escaped)
215 }
216}
217
218impl PartialEq for Name {
219 fn eq(&self, other: &Self) -> bool {
220 self.unescaped == other.unescaped
221 }
222}
223
224impl Eq for Name {}
225
226impl PartialOrd for Name {
227 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
228 Some(self.cmp(other))
229 }
230}
231
232impl Ord for Name {
233 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
234 self.unescaped.cmp(&other.unescaped)
235 }
236}
237
238impl Hash for Name {
239 fn hash<H: Hasher>(&self, state: &mut H) {
240 self.unescaped.hash(state);
241 }
242}
243
244impl TryFrom<&str> for Name {
245 type Error = Error;
246
247 fn try_from(s: &str) -> Result<Self> {
248 Self::try_new(s)
249 }
250}
251
252impl TryFrom<&String> for Name {
253 type Error = Error;
254
255 fn try_from(s: &String) -> Result<Self> {
256 Self::try_new(s.as_str())
257 }
258}
259
260impl TryFrom<String> for Name {
261 type Error = Error;
262
263 fn try_from(s: String) -> Result<Self> {
264 Self::try_new(s)
265 }
266}
267
268impl FromStr for Name {
269 type Err = Error;
270
271 fn from_str(s: &str) -> Result<Self> {
272 Self::try_new(s)
273 }
274}
275
276#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
290#[must_use = "DatabaseName represents a validated database identifier that should not be discarded. Use it in your connection or table definitions"]
291pub struct DatabaseName {
292 name: Name,
293}
294
295impl DatabaseName {
296 pub fn try_new(name: impl Into<String>) -> Result<Self> {
302 Ok(DatabaseName {
303 name: Name::try_new(name)?,
304 })
305 }
306
307 pub fn name(&self) -> &Name {
309 &self.name
310 }
311
312 #[must_use]
314 pub fn unescaped(&self) -> &str {
315 self.name.unescaped()
316 }
317}
318
319impl fmt::Display for DatabaseName {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 write!(f, "{}", self.name)
322 }
323}
324
325impl TryFrom<&str> for DatabaseName {
326 type Error = Error;
327
328 fn try_from(s: &str) -> Result<Self> {
329 Self::try_new(s)
330 }
331}
332
333impl TryFrom<&String> for DatabaseName {
334 type Error = Error;
335
336 fn try_from(s: &String) -> Result<Self> {
337 Self::try_new(s.as_str())
338 }
339}
340
341impl TryFrom<String> for DatabaseName {
342 type Error = Error;
343
344 fn try_from(s: String) -> Result<Self> {
345 Self::try_new(s)
346 }
347}
348
349impl From<Name> for DatabaseName {
350 fn from(name: Name) -> Self {
351 DatabaseName { name }
352 }
353}
354
355impl FromStr for DatabaseName {
356 type Err = Error;
357
358 fn from_str(s: &str) -> Result<Self> {
359 Self::try_new(s)
360 }
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, Hash)]
384#[must_use = "SchemaName represents a validated schema identifier that should not be discarded. Use it in your table definitions or queries"]
385pub struct SchemaName {
386 database: Option<DatabaseName>,
387 schema: Name,
388}
389
390impl SchemaName {
391 pub fn try_new(schema: impl Into<String>) -> Result<Self> {
407 Ok(SchemaName {
408 database: None,
409 schema: Name::try_new(schema)?,
410 })
411 }
412
413 pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
432 self.database = Some(DatabaseName::try_new(database)?);
433 Ok(self)
434 }
435
436 #[must_use]
438 pub fn database(&self) -> Option<&DatabaseName> {
439 self.database.as_ref()
440 }
441
442 pub fn schema(&self) -> &Name {
444 &self.schema
445 }
446
447 #[must_use]
449 pub fn unescaped(&self) -> &str {
450 self.schema.unescaped()
451 }
452}
453
454impl fmt::Display for SchemaName {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 if let Some(ref db) = self.database {
457 write!(f, "{}.{}", db, self.schema)
458 } else {
459 write!(f, "{}", self.schema)
460 }
461 }
462}
463
464impl TryFrom<&str> for SchemaName {
465 type Error = Error;
466
467 fn try_from(s: &str) -> Result<Self> {
468 s.parse()
469 }
470}
471
472impl TryFrom<&String> for SchemaName {
473 type Error = Error;
474
475 fn try_from(s: &String) -> Result<Self> {
476 s.as_str().parse()
477 }
478}
479
480impl TryFrom<String> for SchemaName {
481 type Error = Error;
482
483 fn try_from(s: String) -> Result<Self> {
484 s.parse()
485 }
486}
487
488impl From<Name> for SchemaName {
489 fn from(name: Name) -> Self {
490 SchemaName {
491 database: None,
492 schema: name,
493 }
494 }
495}
496
497impl FromStr for SchemaName {
498 type Err = Error;
499
500 fn from_str(s: &str) -> Result<Self> {
501 let parts = parse_qualified_identifier(s);
502
503 match parts.as_slice() {
505 [s] => SchemaName::try_new(s),
506 [d, s] => SchemaName::try_new(s)?.with_database(d),
507 _ => Err(Error::InvalidName(format!("Invalid SQL identifier: {s}"))),
508 }
509 }
510}
511
512#[derive(Clone, Debug, PartialEq, Eq, Hash)]
540#[must_use = "TableName represents a validated table identifier that should not be discarded. Use it in your queries or table operations"]
541pub struct TableName {
542 database: Option<DatabaseName>,
543 schema: Option<Name>,
544 table: Name,
545}
546
547impl TableName {
548 pub fn try_new(table: impl Into<String>) -> Result<Self> {
564 Ok(TableName {
565 database: None,
566 schema: None,
567 table: Name::try_new(table)?,
568 })
569 }
570
571 pub fn with_schema(mut self, schema: impl Into<String>) -> Result<Self> {
590 self.schema = Some(Name::try_new(schema)?);
591 Ok(self)
592 }
593
594 pub fn with_database(mut self, database: impl Into<String>) -> Result<Self> {
615 self.database = Some(DatabaseName::try_new(database)?);
616 Ok(self)
617 }
618
619 #[must_use]
621 pub fn database(&self) -> Option<&DatabaseName> {
622 self.database.as_ref()
623 }
624
625 #[must_use]
627 pub fn schema(&self) -> Option<&Name> {
628 self.schema.as_ref()
629 }
630
631 pub fn table(&self) -> &Name {
633 &self.table
634 }
635
636 #[must_use]
638 pub fn unescaped(&self) -> &str {
639 self.table.unescaped()
640 }
641}
642
643impl fmt::Display for TableName {
644 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645 if let Some(ref db) = self.database {
646 write!(f, "{db}.")?;
647 }
648 if let Some(ref schema) = self.schema {
649 write!(f, "{schema}.")?;
650 }
651 write!(f, "{}", self.table)
652 }
653}
654
655impl TryFrom<&str> for TableName {
656 type Error = Error;
657
658 fn try_from(s: &str) -> Result<Self> {
659 s.parse()
660 }
661}
662
663impl TryFrom<&String> for TableName {
664 type Error = Error;
665
666 fn try_from(s: &String) -> Result<Self> {
667 s.as_str().parse()
668 }
669}
670
671impl TryFrom<String> for TableName {
672 type Error = Error;
673
674 fn try_from(s: String) -> Result<Self> {
675 s.parse()
676 }
677}
678
679impl From<Name> for TableName {
680 fn from(name: Name) -> Self {
681 TableName {
682 database: None,
683 schema: None,
684 table: name,
685 }
686 }
687}
688
689impl FromStr for TableName {
690 type Err = Error;
691
692 fn from_str(s: &str) -> Result<Self> {
693 let mut parts = Vec::new();
694 let mut current = String::new();
695 let mut in_quotes = false;
696 let mut chars = s.chars().peekable();
697
698 while let Some(c) = chars.next() {
699 match c {
700 '"' => {
702 if in_quotes && chars.peek() == Some(&'"') {
704 current.push('"');
705 chars.next(); } else {
707 in_quotes = !in_quotes;
708 }
710 }
711 '.' if !in_quotes => {
713 if !current.is_empty() {
714 parts.push(current.split_off(0));
715 }
716 }
717 _ => current.push(c),
718 }
719 }
720 if !current.is_empty() {
721 parts.push(current);
722 }
723
724 match parts.as_slice() {
726 [t] => TableName::try_new(t),
727 [s, t] => TableName::try_new(t)?.with_schema(s),
728 [d, s, t] => TableName::try_new(t)?.with_schema(s)?.with_database(d),
729 _ => Err(Error::InvalidName(format!("Invalid SQL identifier: {s}"))),
730 }
731 }
732}
733
734#[macro_export]
758macro_rules! table_name {
759 ($db:expr, $schema:expr, $table:expr) => {
761 $crate::TableName::try_new($table)?
762 .with_schema($schema)?
763 .with_database($db)
764 };
765
766 ($schema:expr, $table:expr) => {
768 $crate::TableName::try_new($table)?.with_schema($schema)
769 };
770
771 ($table:expr) => {
773 $crate::TableName::try_new($table)
774 };
775}
776
777#[macro_export]
797macro_rules! schema_name {
798 ($db:expr, $schema:expr) => {
800 $crate::SchemaName::try_new($schema)?.with_database($db)
801 };
802
803 ($schema:expr) => {
805 $crate::SchemaName::try_new($schema)
806 };
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 #[test]
814 fn test_escape_name() {
815 assert_eq!(escape_name("table").unwrap(), "\"table\"");
816 assert_eq!(escape_name("my_table").unwrap(), "\"my_table\"");
817 assert_eq!(escape_name("table\"quote").unwrap(), "\"table\"\"quote\"");
818 assert_eq!(escape_name("").unwrap(), "\"\"");
819 }
820
821 #[test]
822 fn test_escape_name_too_long() {
823 let max_name = "a".repeat(PG_IDENTIFIER_LIMIT);
825 assert!(escape_name(&max_name).is_ok());
826
827 let too_long = "a".repeat(PG_IDENTIFIER_LIMIT + 1);
829 let err = escape_name(&too_long).unwrap_err();
830 assert!(err.to_string().contains("identifier limit"));
831 }
832
833 #[test]
834 fn test_escape_sql_path() {
835 assert_eq!(escape_sql_path("/tmp/data.hyper"), "\"/tmp/data.hyper\"");
836 assert_eq!(
837 escape_sql_path("/tmp/my \"db\".hyper"),
838 "\"/tmp/my \"\"db\"\".hyper\""
839 );
840 assert_eq!(escape_sql_path(""), "\"\"");
841
842 let long_path = format!("/very/long/path/{}.hyper", "a".repeat(100));
844 let escaped = escape_sql_path(&long_path);
845 assert!(escaped.starts_with('"'));
846 assert!(escaped.ends_with('"'));
847 }
848
849 #[test]
850 fn test_escape_string_literal() {
851 assert_eq!(escape_string_literal("hello"), "'hello'");
852 assert_eq!(escape_string_literal("it's"), "'it''s'");
853 assert_eq!(escape_string_literal(""), "''");
854 }
855
856 #[test]
857 fn test_name() {
858 let name = Name::try_new("users").unwrap();
859 assert_eq!(name.to_string(), "\"users\"");
860 assert_eq!(name.unescaped(), "users");
861 assert!(!name.unescaped().is_empty());
862 }
863
864 #[test]
865 fn test_name_with_quotes() {
866 let name = Name::try_new("table\"name").unwrap();
867 assert_eq!(name.to_string(), "\"table\"\"name\"");
868 assert_eq!(name.unescaped(), "table\"name");
869 }
870
871 #[test]
872 fn test_database_name() {
873 let db = DatabaseName::try_new("mydb").unwrap();
874 assert_eq!(db.to_string(), "\"mydb\"");
875 assert_eq!(db.unescaped(), "mydb");
876 }
877
878 #[test]
879 fn test_schema_name() {
880 let schema = SchemaName::try_new("public").unwrap();
881 assert_eq!(schema.to_string(), "\"public\"");
882
883 let qualified = SchemaName::try_new("public")
884 .unwrap()
885 .with_database("mydb")
886 .unwrap();
887 assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
888 }
889
890 #[test]
891 fn test_table_name() {
892 let simple = TableName::try_new("users").unwrap();
893 assert_eq!(simple.to_string(), "\"users\"");
894
895 let with_schema = TableName::try_new("users")
896 .unwrap()
897 .with_schema("public")
898 .unwrap();
899 assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
900
901 let full = TableName::try_new("users")
902 .unwrap()
903 .with_schema("public")
904 .unwrap()
905 .with_database("mydb")
906 .unwrap();
907 assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
908 }
909
910 #[test]
911 fn test_name_equality() {
912 let name1 = Name::try_new("test").unwrap();
913 let name2 = Name::try_new("test").unwrap();
914 let name3 = Name::try_new("other").unwrap();
915
916 assert_eq!(name1, name2);
917 assert_ne!(name1, name3);
918 }
919
920 #[test]
921 fn test_schema_name_from_str() {
922 let schema: SchemaName = "public".parse().unwrap();
924 assert_eq!(schema.to_string(), "\"public\"");
925 assert_eq!(schema.unescaped(), "public");
926
927 let qualified: SchemaName = "mydb.public".parse().unwrap();
929 assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
930 assert_eq!(qualified.unescaped(), "public");
931 assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
932
933 let quoted: SchemaName = "\"my db\".\"my schema\"".parse().unwrap();
935 assert_eq!(quoted.to_string(), "\"my db\".\"my schema\"");
936 assert_eq!(quoted.unescaped(), "my schema");
937
938 let escaped: SchemaName = "\"schema\"\"name\"".parse().unwrap();
940 assert_eq!(escaped.to_string(), "\"schema\"\"name\"");
941 assert_eq!(escaped.unescaped(), "schema\"name");
942
943 assert!("db.schema.table".parse::<SchemaName>().is_err());
945 assert!("".parse::<SchemaName>().is_err());
946 }
947
948 #[test]
949 fn test_table_name_from_str() {
950 let table: TableName = "users".parse().unwrap();
952 assert_eq!(table.to_string(), "\"users\"");
953 assert_eq!(table.unescaped(), "users");
954
955 let with_schema: TableName = "public.users".parse().unwrap();
957 assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
958 assert_eq!(with_schema.unescaped(), "users");
959 assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
960
961 let full: TableName = "mydb.public.users".parse().unwrap();
963 assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
964 assert_eq!(full.unescaped(), "users");
965 assert_eq!(full.schema().unwrap().unescaped(), "public");
966 assert_eq!(full.database().unwrap().unescaped(), "mydb");
967
968 let quoted: TableName = "\"my db\".\"my schema\".\"my table\"".parse().unwrap();
970 assert_eq!(quoted.to_string(), "\"my db\".\"my schema\".\"my table\"");
971 assert_eq!(quoted.unescaped(), "my table");
972
973 let escaped: TableName = "\"table\"\"name\"".parse().unwrap();
975 assert_eq!(escaped.to_string(), "\"table\"\"name\"");
976 assert_eq!(escaped.unescaped(), "table\"name");
977
978 let with_dots: TableName = "\"schema.name\".\"table.name\"".parse().unwrap();
980 assert_eq!(with_dots.to_string(), "\"schema.name\".\"table.name\"");
981 assert_eq!(with_dots.schema().unwrap().unescaped(), "schema.name");
982 assert_eq!(with_dots.unescaped(), "table.name");
983
984 assert!("db.schema.table.extra".parse::<TableName>().is_err());
986 assert!("".parse::<TableName>().is_err());
987 }
988
989 #[test]
990 fn test_schema_name_macro() -> Result<()> {
991 let schema = schema_name!("public")?;
993 assert_eq!(schema.to_string(), "\"public\"");
994 assert_eq!(schema.unescaped(), "public");
995
996 let qualified = schema_name!("mydb", "public")?;
998 assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
999 assert_eq!(qualified.unescaped(), "public");
1000 assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1001 Ok(())
1002 }
1003
1004 #[test]
1005 fn test_table_name_macro() -> Result<()> {
1006 let table = table_name!("users")?;
1008 assert_eq!(table.to_string(), "\"users\"");
1009 assert_eq!(table.unescaped(), "users");
1010
1011 let with_schema = table_name!("public", "users")?;
1013 assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1014 assert_eq!(with_schema.unescaped(), "users");
1015 assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1016
1017 let full = table_name!("mydb", "public", "users")?;
1019 assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1020 assert_eq!(full.unescaped(), "users");
1021 assert_eq!(full.schema().unwrap().unescaped(), "public");
1022 assert_eq!(full.database().unwrap().unescaped(), "mydb");
1023 Ok(())
1024 }
1025
1026 #[test]
1027 fn test_schema_name_try_from() {
1028 let schema: SchemaName = "public".try_into().unwrap();
1030 assert_eq!(schema.to_string(), "\"public\"");
1031 assert_eq!(schema.unescaped(), "public");
1032
1033 let qualified: SchemaName = "mydb.public".try_into().unwrap();
1035 assert_eq!(qualified.to_string(), "\"mydb\".\"public\"");
1036 assert_eq!(qualified.unescaped(), "public");
1037 assert_eq!(qualified.database().unwrap().unescaped(), "mydb");
1038
1039 let schema_string: SchemaName = String::from("public").try_into().unwrap();
1041 assert_eq!(schema_string.to_string(), "\"public\"");
1042 }
1043
1044 #[test]
1045 fn test_table_name_try_from() {
1046 let table: TableName = "users".try_into().unwrap();
1048 assert_eq!(table.to_string(), "\"users\"");
1049 assert_eq!(table.unescaped(), "users");
1050
1051 let with_schema: TableName = "public.users".try_into().unwrap();
1053 assert_eq!(with_schema.to_string(), "\"public\".\"users\"");
1054 assert_eq!(with_schema.unescaped(), "users");
1055 assert_eq!(with_schema.schema().unwrap().unescaped(), "public");
1056
1057 let full: TableName = "mydb.public.users".try_into().unwrap();
1059 assert_eq!(full.to_string(), "\"mydb\".\"public\".\"users\"");
1060 assert_eq!(full.unescaped(), "users");
1061 assert_eq!(full.schema().unwrap().unescaped(), "public");
1062 assert_eq!(full.database().unwrap().unescaped(), "mydb");
1063
1064 let table_string: TableName = String::from("users").try_into().unwrap();
1066 assert_eq!(table_string.to_string(), "\"users\"");
1067
1068 let invalid: std::result::Result<TableName, _> = "db.schema.table.extra".try_into();
1070 assert!(invalid.is_err());
1071 }
1072}