Skip to content

Commit ef2ac17

Browse files
committed
refactor(WIP)!: convertions from types that makes sense on the join clause params
1 parent 67eec2a commit ef2ac17

6 files changed

Lines changed: 332 additions & 69 deletions

File tree

canyon_core/src/query/bounds.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub trait Inspectionable<'a> {
3838
}
3939

4040
pub trait TableMetadata<'a>: std::fmt::Display {
41-
fn as_str(&self) -> &'static str;
41+
fn as_str(&self) -> &'a str;
4242
}
4343

4444
/// Created for retrieve the field's name of a field of a struct, giving

canyon_core/src/query/querybuilder/syntax/column.rs

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::query::bounds::FieldIdentifier;
12
use crate::query::querybuilder::syntax::symbol::Symbol::Dot;
23
use crate::query::querybuilder::syntax::tokens::{SqlToken, ToSqlTokens};
34

@@ -8,9 +9,18 @@ pub struct ColumnRef<'a> {
89
pub alias: Option<&'a str>,
910
}
1011

12+
impl<'a, T> From<T> for ColumnRef<'a>
13+
where
14+
T: FieldIdentifier,
15+
{
16+
fn from(value: T) -> Self {
17+
Self::from(value.as_str())
18+
}
19+
}
20+
1121
impl<'a> From<&'a str> for ColumnRef<'a> {
1222
fn from(value: &'a str) -> Self {
13-
Self::new(value)
23+
__impl::column_ref_from_str_ref(value)
1424
}
1525
}
1626

@@ -32,7 +42,11 @@ impl<'a> ToSqlTokens<'a> for ColumnRef<'a> {
3242

3343
impl<'a> ColumnRef<'a> {
3444
pub fn new(column_name: &'a str) -> Self {
35-
Self { column: column_name, table: None, alias: None }
45+
Self {
46+
column: column_name,
47+
table: None,
48+
alias: None,
49+
}
3650
}
3751

3852
/// mutator that allows to modify a [`ColumnRef`] to have a <table>.<column> format
@@ -52,3 +66,224 @@ impl<'a> ColumnRef<'a> {
5266
self
5367
}
5468
}
69+
70+
mod __impl {
71+
use crate::query::querybuilder::syntax::column::{__detail, ColumnRef};
72+
73+
pub(crate) fn column_ref_from_str_ref(value: &'_ str) -> ColumnRef<'_> {
74+
let trimmed = value.trim();
75+
76+
let (before_alias, alias) = match __detail::find_case_insensitive_as(trimmed) {
77+
Some(idx) => {
78+
let (left, right) = trimmed.split_at(idx);
79+
let right = right[2..].trim_start();
80+
(left.trim(), Some(right.trim()))
81+
}
82+
None => (trimmed, None),
83+
};
84+
85+
let (table, column) = match before_alias.split_once('.') {
86+
Some((tbl, col)) => (Some(tbl.trim()), col.trim()),
87+
None => (None, before_alias.trim()),
88+
};
89+
90+
ColumnRef {
91+
table,
92+
column,
93+
alias,
94+
}
95+
}
96+
}
97+
98+
mod __detail {
99+
pub(crate) fn find_case_insensitive_as(s: &str) -> Option<usize> {
100+
let bytes = s.as_bytes();
101+
for i in 0..bytes.len().saturating_sub(2) {
102+
let a = bytes[i];
103+
let b = bytes[i + 1];
104+
105+
// Match case-insensitive ASCII
106+
let is_a = a == b'a' || a == b'A';
107+
let is_s = b == b's' || b == b'S';
108+
109+
if is_a && is_s {
110+
let before_ok = i > 0 && bytes[i - 1].is_ascii_whitespace();
111+
let after_ok = i + 2 < bytes.len() && bytes[i + 2].is_ascii_whitespace();
112+
113+
if before_ok && after_ok {
114+
return Some(i);
115+
}
116+
}
117+
}
118+
None
119+
}
120+
}
121+
122+
#[cfg(test)]
123+
mod tests {
124+
use super::ColumnRef;
125+
use crate::query::querybuilder::syntax::column::__detail::find_case_insensitive_as;
126+
127+
// ------------------------------------------------------------
128+
// Tests for find_case_insensitive_as
129+
// ------------------------------------------------------------
130+
#[test]
131+
fn test_find_as_basic_uppercase() {
132+
let idx = find_case_insensitive_as("col AS x").unwrap();
133+
assert_eq!(&"col AS x"[idx..idx + 2], "AS");
134+
}
135+
136+
#[test]
137+
fn test_find_as_lowercase() {
138+
let idx = find_case_insensitive_as("col as x").unwrap();
139+
assert_eq!(&"col as x"[idx..idx + 2], "as");
140+
}
141+
142+
#[test]
143+
fn test_find_as_mixed_case() {
144+
let idx = find_case_insensitive_as("col As x").unwrap();
145+
assert_eq!(&"col As x"[idx..idx + 2], "As");
146+
}
147+
148+
#[test]
149+
fn test_find_as_with_multiple_spaces() {
150+
let idx = find_case_insensitive_as("col AS x").unwrap();
151+
assert_eq!(&"col AS x"[idx..idx + 2], "AS");
152+
}
153+
154+
#[test]
155+
fn test_find_as_requires_space_before_and_after() {
156+
assert!(find_case_insensitive_as("colASx").is_none());
157+
assert!(find_case_insensitive_as("col ASx").is_none());
158+
assert!(find_case_insensitive_as("colAS x").is_none());
159+
assert!(find_case_insensitive_as("ASx").is_none());
160+
assert!(find_case_insensitive_as("xAS").is_none());
161+
}
162+
163+
#[test]
164+
fn test_find_as_at_start_or_end() {
165+
assert!(find_case_insensitive_as(" AS x").is_some());
166+
assert!(find_case_insensitive_as("x AS ").is_some());
167+
}
168+
169+
#[test]
170+
fn test_find_as_no_match() {
171+
assert!(find_case_insensitive_as("column something").is_none());
172+
assert!(find_case_insensitive_as("").is_none());
173+
assert!(find_case_insensitive_as("a s").is_none());
174+
assert!(find_case_insensitive_as("col AX x").is_none());
175+
}
176+
177+
#[test]
178+
fn test_find_as_with_table_column() {
179+
let idx = find_case_insensitive_as("table.col as alias").unwrap();
180+
assert_eq!(&"table.col as alias"[idx..idx + 2], "as");
181+
}
182+
183+
// ------------------------------------------------------------
184+
// Tests for From<&str> for ColumnRef
185+
// ------------------------------------------------------------
186+
#[test]
187+
fn test_column_ref_simple_column() {
188+
let c = ColumnRef::from("name");
189+
assert_eq!(c.table, None);
190+
assert_eq!(c.column, "name");
191+
assert_eq!(c.alias, None);
192+
}
193+
194+
#[test]
195+
fn test_column_ref_table_column() {
196+
let c = ColumnRef::from("users.name");
197+
assert_eq!(c.table, Some("users"));
198+
assert_eq!(c.column, "name");
199+
assert_eq!(c.alias, None);
200+
}
201+
202+
#[test]
203+
fn test_column_ref_with_alias_uppercase_as() {
204+
let c = ColumnRef::from("users.name AS n");
205+
assert_eq!(c.table, Some("users"));
206+
assert_eq!(c.column, "name");
207+
assert_eq!(c.alias, Some("n"));
208+
}
209+
210+
#[test]
211+
fn test_column_ref_with_alias_lowercase_as() {
212+
let c = ColumnRef::from("users.name as n");
213+
assert_eq!(c.table, Some("users"));
214+
assert_eq!(c.column, "name");
215+
assert_eq!(c.alias, Some("n"));
216+
}
217+
218+
#[test]
219+
fn test_column_ref_with_alias_mixed_case_as() {
220+
let c = ColumnRef::from("users.name As n");
221+
assert_eq!(c.table, Some("users"));
222+
assert_eq!(c.column, "name");
223+
assert_eq!(c.alias, Some("n"));
224+
}
225+
226+
#[test]
227+
fn test_column_ref_multiple_spaces_around_as() {
228+
let c = ColumnRef::from("users.name AS n");
229+
assert_eq!(c.table, Some("users"));
230+
assert_eq!(c.column, "name");
231+
assert_eq!(c.alias, Some("n"));
232+
}
233+
234+
#[test]
235+
fn test_column_ref_alias_without_table() {
236+
let c = ColumnRef::from("name AS n");
237+
assert_eq!(c.table, None);
238+
assert_eq!(c.column, "name");
239+
assert_eq!(c.alias, Some("n"));
240+
}
241+
242+
#[test]
243+
fn test_column_ref_no_alias_when_as_not_valid() {
244+
let c = ColumnRef::from("nameASn");
245+
assert_eq!(c.table, None);
246+
assert_eq!(c.column, "nameASn");
247+
assert_eq!(c.alias, None);
248+
}
249+
250+
#[test]
251+
fn test_column_ref_trim_whitespace() {
252+
let c = ColumnRef::from(" users.name AS n ");
253+
assert_eq!(c.table, Some("users"));
254+
assert_eq!(c.column, "name");
255+
assert_eq!(c.alias, Some("n"));
256+
}
257+
258+
#[test]
259+
fn test_column_ref_alias_complex() {
260+
let c = ColumnRef::from("users.full_name AS fullNameAlias");
261+
assert_eq!(c.table, Some("users"));
262+
assert_eq!(c.column, "full_name");
263+
assert_eq!(c.alias, Some("fullNameAlias"));
264+
}
265+
266+
#[test]
267+
fn test_column_ref_no_table_but_alias() {
268+
let c = ColumnRef::from("email AS e");
269+
assert_eq!(c.table, None);
270+
assert_eq!(c.column, "email");
271+
assert_eq!(c.alias, Some("e"));
272+
}
273+
274+
#[test]
275+
fn test_column_ref_only_column_and_spaces() {
276+
let c = ColumnRef::from(" column_name ");
277+
assert_eq!(c.table, None);
278+
assert_eq!(c.column, "column_name");
279+
assert_eq!(c.alias, None);
280+
}
281+
282+
#[test]
283+
fn test_column_ref_only_table_column_with_spaces() {
284+
let c = ColumnRef::from(" users . name ");
285+
assert_eq!(c.table, Some("users"));
286+
assert_eq!(c.column, "name");
287+
assert_eq!(c.alias, None);
288+
}
289+
}

canyon_core/src/query/querybuilder/syntax/table_metadata.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
use std::borrow::Cow;
2-
use std::fmt::{Display, Formatter};
1+
use crate::query::bounds;
32
use crate::query::querybuilder::syntax::symbol::Symbol;
43
use crate::query::querybuilder::syntax::tokens::{SqlToken, ToSqlTokens};
4+
use std::borrow::Cow;
5+
use std::fmt::{Display, Formatter};
56

67
#[derive(Clone, Default, Debug)]
78
pub struct TableMetadata<'a> {
89
pub schema: Option<Cow<'a, str>>,
910
pub name: Cow<'a, str>,
10-
} // TODO: we can have those fields as Cow<'_> for max performance
11+
}
12+
13+
impl<'a, T> From<T> for TableMetadata<'a>
14+
where
15+
T: bounds::TableMetadata<'a>,
16+
{
17+
fn from(value: T) -> Self {
18+
Self::from(value.as_str()) // this covers the need of producing <table>.<column>
19+
}
20+
}
1121

1222
impl<'a> ToSqlTokens<'a> for TableMetadata<'a> {
1323
fn to_tokens(&self, out: &mut Vec<SqlToken<'a>>) {
@@ -38,13 +48,19 @@ impl<'a> From<&'a str> for TableMetadata<'a> {
3848
}
3949
}
4050

41-
4251
impl<'a> TableMetadata<'a> {
4352
pub fn new(table_name: &'a str) -> Self {
44-
Self { schema: None, name: Cow::from(table_name) }
53+
Self {
54+
schema: None,
55+
name: Cow::from(table_name),
56+
}
57+
}
58+
pub fn schema(&mut self, schema: String) {
59+
self.schema = Some(Cow::from(schema));
60+
}
61+
pub fn table_name(&mut self, table_name: String) {
62+
self.name = Cow::from(table_name)
4563
}
46-
pub fn schema(&mut self, schema: String) { self.schema = Some(Cow::from(schema)); }
47-
pub fn table_name(&mut self, table_name: String) { self.name = Cow::from(table_name) }
4864

4965
/// Returns an already formatted version of the schema and table of a target database table
5066
/// ready to be used in a SQL statement.
@@ -54,17 +70,23 @@ impl<'a> TableMetadata<'a> {
5470
/// and there's some heavy callee procedure
5571
pub fn sql(&self) -> String {
5672
match &self.schema {
57-
Some(schema_name) => {format!("{}.{}", schema_name, self.name)}
58-
None => self.name.to_string()
73+
Some(schema_name) => {
74+
format!("{}.{}", schema_name, self.name)
75+
}
76+
None => self.name.to_string(),
5977
}
6078
}
6179
}
6280

6381
impl<'a> Display for TableMetadata<'a> {
6482
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
6583
match &self.schema {
66-
Some(schema_name) => {write!(f, "{}.{}", schema_name, self.name)}
67-
None => {write!(f, "{}", self.name)}
84+
Some(schema_name) => {
85+
write!(f, "{}.{}", schema_name, self.name)
86+
}
87+
None => {
88+
write!(f, "{}", self.name)
89+
}
6890
}
6991
}
7092
}

canyon_entities/src/manager_builder.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ pub fn generated_enum_type_for_struct_data(canyon_entity: &CanyonEntity) -> Toke
7373
}
7474
}
7575

76-
impl #generics canyon_sql::query::bounds::TableMetadata<'_> for #generics #enum_name #generics {
77-
fn as_str(&self) -> &'static str {
76+
impl<'a> canyon_sql::query::bounds::TableMetadata<'a> for #generics #enum_name #generics {
77+
fn as_str(&self) -> &'a str {
7878
match *self {
7979
#enum_name::Name => #struct_name,
8080
#enum_name::DbName => #db_target_table_name,

canyon_macros/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub fn main(_meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerT
8181
/// the tokio's current reactor
8282
#[proc_macro_attribute]
8383
pub fn canyon_tokio_test(
84+
// TODO: with Result for using ? on tests?
8485
_meta: CompilerTokenStream,
8586
input: CompilerTokenStream,
8687
) -> CompilerTokenStream {
@@ -119,6 +120,7 @@ pub fn canyon_tokio_test(
119120
#[proc_macro_attribute]
120121
pub fn canyon_entity(meta: CompilerTokenStream, input: CompilerTokenStream) -> CompilerTokenStream {
121122
let attrs = syn::parse_macro_input!(meta as syn::AttributeArgs);
123+
// TODO: a table with table and schema fields maybe would have been more corret
122124
generate_canyon_entity_tokens(attrs, input).into()
123125
}
124126

0 commit comments

Comments
 (0)