Skip to main content

toasty_driver_integration_suite/tests/
starts_with.rs

1use crate::prelude::*;
2
3/// Model with a composite key (partition + sort) and a non-key string attribute.
4/// Used for all starts_with tests.
5#[derive(Debug, toasty::Model)]
6#[key(partition = partition_id, local = sort_key)]
7struct Item {
8    partition_id: i64,
9    sort_key: String,
10    name: String,
11}
12
13async fn setup(test: &mut Test) -> toasty::Db {
14    let mut db = test.setup_db(models!(Item)).await;
15
16    toasty::create!(Item::[
17        { partition_id: 1_i64, sort_key: "alpha-1", name: "Alice" },
18        { partition_id: 1_i64, sort_key: "alpha-2", name: "Alicia" },
19        { partition_id: 1_i64, sort_key: "beta-1",  name: "Bob"   },
20        { partition_id: 1_i64, sort_key: "beta-2",  name: "Barry" },
21        { partition_id: 2_i64, sort_key: "alpha-1", name: "Carol" },
22    ])
23    .exec(&mut db)
24    .await
25    .unwrap();
26
27    db
28}
29
30/// starts_with on the sort key. On DynamoDB this uses KeyConditionExpression;
31/// on SQL it lowers to LIKE.
32#[driver_test]
33pub async fn starts_with_sort_key(test: &mut Test) -> Result<()> {
34    let mut db = setup(test).await;
35
36    let mut items: Vec<Item> = Item::filter(
37        Item::fields()
38            .partition_id()
39            .eq(1_i64)
40            .and(Item::fields().sort_key().starts_with("alpha".to_string())),
41    )
42    .exec(&mut db)
43    .await?;
44
45    items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
46
47    assert_eq!(items.len(), 2);
48    assert_eq!(items[0].sort_key, "alpha-1");
49    assert_eq!(items[1].sort_key, "alpha-2");
50
51    Ok(())
52}
53
54/// starts_with on a non-key attribute. On DynamoDB this uses FilterExpression;
55/// on SQL it lowers to LIKE.
56#[driver_test]
57pub async fn starts_with_non_key_attr(test: &mut Test) -> Result<()> {
58    let mut db = setup(test).await;
59
60    let mut items: Vec<Item> = Item::filter(
61        Item::fields()
62            .partition_id()
63            .eq(1_i64)
64            .and(Item::fields().name().starts_with("Al".to_string())),
65    )
66    .exec(&mut db)
67    .await?;
68
69    items.sort_by(|a, b| a.name.cmp(&b.name));
70
71    assert_eq!(items.len(), 2);
72    assert_eq!(items[0].name, "Alice");
73    assert_eq!(items[1].name, "Alicia");
74
75    Ok(())
76}
77
78/// starts_with with a prefix that matches nothing — returns empty result.
79#[driver_test]
80pub async fn starts_with_no_match(test: &mut Test) -> Result<()> {
81    let mut db = setup(test).await;
82
83    let items: Vec<Item> = Item::filter(
84        Item::fields()
85            .partition_id()
86            .eq(1_i64)
87            .and(Item::fields().sort_key().starts_with("gamma".to_string())),
88    )
89    .exec(&mut db)
90    .await?;
91
92    assert_eq!(items.len(), 0);
93
94    Ok(())
95}
96
97/// starts_with with an empty prefix — DynamoDB rejects empty string key values.
98#[driver_test(requires(not(sql)))]
99pub async fn starts_with_empty_prefix(test: &mut Test) -> Result<()> {
100    let mut db = setup(test).await;
101
102    let result: toasty::Result<Vec<Item>> = Item::filter(
103        Item::fields()
104            .partition_id()
105            .eq(1_i64)
106            .and(Item::fields().sort_key().starts_with("".to_string())),
107    )
108    .exec(&mut db)
109    .await;
110
111    assert!(
112        result.is_err(),
113        "expected error when using starts_with with empty prefix on DynamoDB"
114    );
115
116    Ok(())
117}
118
119/// starts_with with an empty prefix on SQL — lowers to LIKE '%', matches all rows.
120#[driver_test(requires(sql))]
121pub async fn starts_with_empty_prefix_sql(test: &mut Test) -> Result<()> {
122    let mut db = setup(test).await;
123
124    let items: Vec<Item> = Item::filter(
125        Item::fields()
126            .partition_id()
127            .eq(1_i64)
128            .and(Item::fields().sort_key().starts_with("".to_string())),
129    )
130    .exec(&mut db)
131    .await?;
132
133    assert_eq!(items.len(), 4, "empty prefix should match all rows on SQL");
134
135    Ok(())
136}
137
138/// starts_with prefix containing SQL LIKE wildcards (`%`, `_`) and the
139/// chosen escape char (`!`). On SQL drivers the prefix is escaped before
140/// being lowered to LIKE so these characters match literally.
141#[driver_test]
142pub async fn starts_with_special_chars(test: &mut Test) -> Result<()> {
143    #[derive(Debug, toasty::Model)]
144    #[key(partition = partition_id, local = sort_key)]
145    struct StringItem {
146        partition_id: i64,
147        sort_key: String,
148    }
149
150    let mut db = test.setup_db(models!(StringItem)).await;
151
152    toasty::create!(StringItem::[
153        { partition_id: 1_i64, sort_key: "100%-discount" },
154        { partition_id: 1_i64, sort_key: "100xdiscount"  },
155        { partition_id: 1_i64, sort_key: "1009"          },
156        { partition_id: 1_i64, sort_key: "a_b-literal"   },
157        { partition_id: 1_i64, sort_key: "axb-wildcard"  },
158        { partition_id: 1_i64, sort_key: "!bang-literal" },
159        { partition_id: 1_i64, sort_key: "x!bang"        },
160    ])
161    .exec(&mut db)
162    .await
163    .unwrap();
164
165    // `%` must match literally, not as a wildcard.
166    let mut items: Vec<StringItem> = StringItem::filter(
167        StringItem::fields().partition_id().eq(1_i64).and(
168            StringItem::fields()
169                .sort_key()
170                .starts_with("100%".to_string()),
171        ),
172    )
173    .exec(&mut db)
174    .await?;
175    items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
176    assert_eq!(items.len(), 1);
177    assert_eq!(items[0].sort_key, "100%-discount");
178
179    // `_` must match literally, not as a single-char wildcard.
180    let mut items: Vec<StringItem> = StringItem::filter(
181        StringItem::fields().partition_id().eq(1_i64).and(
182            StringItem::fields()
183                .sort_key()
184                .starts_with("a_b".to_string()),
185        ),
186    )
187    .exec(&mut db)
188    .await?;
189    items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
190    assert_eq!(items.len(), 1);
191    assert_eq!(items[0].sort_key, "a_b-literal");
192
193    // `!` (the escape char chosen by the SQL lowering) must also match
194    // literally when present in the user-supplied prefix.
195    let mut items: Vec<StringItem> = StringItem::filter(
196        StringItem::fields().partition_id().eq(1_i64).and(
197            StringItem::fields()
198                .sort_key()
199                .starts_with("!bang".to_string()),
200        ),
201    )
202    .exec(&mut db)
203    .await?;
204    items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
205    assert_eq!(items.len(), 1);
206    assert_eq!(items[0].sort_key, "!bang-literal");
207
208    Ok(())
209}
210
211/// starts_with on an `Option<String>` field — matches non-null values with
212/// the given prefix; rows with NULL values are excluded.
213#[driver_test]
214pub async fn starts_with_optional_field(test: &mut Test) -> Result<()> {
215    #[derive(Debug, toasty::Model)]
216    #[key(partition = partition_id, local = id)]
217    struct OptItem {
218        partition_id: i64,
219        id: i64,
220        nickname: Option<String>,
221    }
222
223    let mut db = test.setup_db(models!(OptItem)).await;
224
225    toasty::create!(OptItem::[
226        { partition_id: 1_i64, id: 1_i64, nickname: Some("Ali".to_string())     },
227        { partition_id: 1_i64, id: 2_i64, nickname: Some("Alicia".to_string())  },
228        { partition_id: 1_i64, id: 3_i64, nickname: Some("Bob".to_string())     },
229        { partition_id: 1_i64, id: 4_i64, nickname: None                        },
230    ])
231    .exec(&mut db)
232    .await?;
233
234    let mut items: Vec<OptItem> = OptItem::filter(
235        OptItem::fields()
236            .partition_id()
237            .eq(1_i64)
238            .and(OptItem::fields().nickname().starts_with("Al".to_string())),
239    )
240    .exec(&mut db)
241    .await?;
242
243    items.sort_by_key(|i| i.id);
244
245    assert_eq!(items.len(), 2);
246    assert_eq!(items[0].nickname.as_deref(), Some("Ali"));
247    assert_eq!(items[1].nickname.as_deref(), Some("Alicia"));
248
249    Ok(())
250}
251
252/// starts_with on the partition key — DynamoDB returns a runtime error since
253/// starts_with is not valid in a KeyConditionExpression on the partition key.
254#[driver_test(requires(not(sql)))]
255pub async fn starts_with_partition_key_error(test: &mut Test) -> Result<()> {
256    #[derive(Debug, toasty::Model)]
257    #[key(partition = partition_id, local = sort_key)]
258    struct StringKeyItem {
259        partition_id: String,
260        sort_key: String,
261    }
262
263    let mut db = test.setup_db(models!(StringKeyItem)).await;
264
265    StringKeyItem::create()
266        .partition_id("hello")
267        .sort_key("world")
268        .exec(&mut db)
269        .await?;
270
271    let result = StringKeyItem::filter(
272        StringKeyItem::fields()
273            .partition_id()
274            .starts_with("hel".to_string()),
275    )
276    .exec(&mut db)
277    .await;
278
279    assert!(
280        result.is_err(),
281        "expected error when using starts_with on partition key"
282    );
283
284    Ok(())
285}