dynamodb_facade/expressions/key_conditions.rs
1use core::fmt;
2use std::marker::PhantomData;
3
4use crate::{AttributeDefinition, CompositeKeySchema, Condition, IntoAttributeValue, KeySchema};
5
6mod sealed_traits {
7 pub trait KeyConditionStateSeal {}
8}
9
10crate::utils::impl_sealed_marker_types!(
11 /// Sealed typestate marker for [`KeyCondition`] build stages.
12 ///
13 /// This trait is sealed and cannot be implemented outside this crate. The two
14 /// implementing types are hidden from public docs:
15 ///
16 /// - `PkOnly` — only the partition key has been set; sort key methods are
17 /// available for composite-key schemas.
18 /// - `WithSk` — a sort key condition has been added; no further SK methods
19 /// are available.
20 ///
21 /// You may encounter this trait as a bound on [`KeyCondition`]'s type
22 /// parameter `S`, but you never need to name the concrete marker types
23 /// directly.
24 KeyConditionState,
25 sealed_traits::KeyConditionStateSeal;
26 #[doc(hidden)]
27 PkOnly,
28 #[doc(hidden)]
29 WithSk
30);
31
32/// Builder for DynamoDB key condition expressions.
33///
34/// `KeyCondition` builds the `KeyConditionExpression` used in Query
35/// operations. It enforces that:
36///
37/// 1. A partition key equality condition is always provided (via [`pk`](KeyCondition::pk)).
38/// 2. Sort key methods (`sk_eq`, `sk_begins_with`, etc.) are only available
39/// for composite-key schemas (tables or indexes with a sort key).
40/// 3. At most one sort key condition can be added, as required by the
41/// DynamoDB API.
42///
43/// In practice you rarely name `KS` or `S` directly — they are inferred from
44/// the item type's key schema when using the generated `T::key_condition(pk_id)`
45/// helper.
46///
47/// # Examples
48///
49/// PK-only query (all enrollments for a user):
50///
51/// ```
52/// # use dynamodb_facade::test_fixtures::*;
53/// use dynamodb_facade::{KeyCondition, TableSchema};
54///
55/// // Targets all items with PK = "USER#user-1"
56/// let kc = KeyCondition::<TableSchema<PlatformTable>>::pk("USER#user-1");
57/// assert_eq!(format!("{kc}"), r#"PK = S("USER#user-1")"#);
58///
59/// // Or you may prefer for the same result:
60/// use dynamodb_facade::DynamoDBItemOp;
61/// let kc = User::key_condition("user-1");
62/// assert_eq!(format!("{kc}"), r#"PK = S("USER#user-1")"#);
63/// ```
64///
65/// PK + SK prefix query:
66///
67/// ```
68/// # use dynamodb_facade::test_fixtures::*;
69/// use dynamodb_facade::{KeyCondition, TableSchema};
70///
71/// let kc = KeyCondition::<TableSchema<PlatformTable>>::pk("USER#user-1")
72/// .sk_begins_with("ENROLL#");
73/// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND begins_with(SK, S("ENROLL#")))"#);
74///
75/// // Or you may prefer for the same result:
76/// use dynamodb_facade::DynamoDBItemOp;
77/// let kc = Enrollment::key_condition("user-1").sk_begins_with("ENROLL#");
78/// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND begins_with(SK, S("ENROLL#")))"#);
79/// ```
80#[derive(Debug, Clone)]
81#[must_use = "key condition does nothing until applied to a request"]
82pub struct KeyCondition<'a, KS: KeySchema, S: KeyConditionState = PkOnly>(
83 Condition<'a>,
84 PhantomData<(KS, S)>,
85);
86
87// -- Initial state: only pk_eq available --------------------------------------
88
89impl<'a, KS: KeySchema> KeyCondition<'a, KS> {
90 /// Creates a key condition with a partition key equality constraint: `PK = value`.
91 ///
92 /// This is the required starting point for all key conditions. The partition
93 /// key attribute name is taken from the key schema `KS` at compile time.
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// # use dynamodb_facade::test_fixtures::*;
99 /// use dynamodb_facade::{KeyCondition, TableSchema};
100 ///
101 /// let kc = KeyCondition::<TableSchema<PlatformTable>>::pk("USER#user-1");
102 /// assert_eq!(format!("{kc}"), r#"PK = S("USER#user-1")"#);
103 ///
104 /// // Or you may prefer for the same result:
105 /// use dynamodb_facade::DynamoDBItemOp;
106 /// let kc = User::key_condition("user-1");
107 /// assert_eq!(format!("{kc}"), r#"PK = S("USER#user-1")"#);
108 /// ```
109 pub fn pk(value: impl IntoAttributeValue) -> Self {
110 KeyCondition(Condition::eq(KS::PartitionKey::NAME, value), PhantomData)
111 }
112}
113// -- PK state: SK methods available (gated by CompositeKeySchema) -------------
114
115impl<'a, KS: CompositeKeySchema> KeyCondition<'a, KS> {
116 /// Adds a sort key equality constraint: `SK = value`.
117 ///
118 /// # Examples
119 ///
120 /// ```
121 /// # use dynamodb_facade::test_fixtures::*;
122 /// use dynamodb_facade::DynamoDBItemOp;
123 /// let kc = Enrollment::key_condition("user-1").sk_eq("ENROLL#course-42");
124 /// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND SK = S("ENROLL#course-42"))"#);
125 /// ```
126 pub fn sk_eq(self, value: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
127 KeyCondition(
128 self.0 & Condition::eq(KS::SortKey::NAME, value),
129 PhantomData,
130 )
131 }
132
133 /// Adds a sort key less-than constraint: `SK < value`.
134 ///
135 /// # Examples
136 ///
137 /// ```
138 /// # use dynamodb_facade::test_fixtures::*;
139 /// use dynamodb_facade::DynamoDBItemOp;
140 ///
141 /// let kc = Enrollment::key_condition("user-1").sk_lt("ENROLL#z");
142 /// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND SK < S("ENROLL#z"))"#);
143 /// ```
144 pub fn sk_lt(self, value: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
145 KeyCondition(
146 self.0 & Condition::lt(KS::SortKey::NAME, value),
147 PhantomData,
148 )
149 }
150
151 /// Adds a sort key less-than-or-equal constraint: `SK <= value`.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// # use dynamodb_facade::test_fixtures::*;
157 /// use dynamodb_facade::DynamoDBItemOp;
158 ///
159 /// let kc = Enrollment::key_condition("user-1").sk_le("ENROLL#z");
160 /// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND SK <= S("ENROLL#z"))"#);
161 /// ```
162 pub fn sk_le(self, value: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
163 KeyCondition(
164 self.0 & Condition::le(KS::SortKey::NAME, value),
165 PhantomData,
166 )
167 }
168
169 /// Adds a sort key greater-than constraint: `SK > value`.
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// # use dynamodb_facade::test_fixtures::*;
175 /// use dynamodb_facade::DynamoDBItemOp;
176 ///
177 /// let kc = Enrollment::key_condition("user-1").sk_gt("ENROLL#2024");
178 /// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND SK > S("ENROLL#2024"))"#);
179 /// ```
180 pub fn sk_gt(self, value: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
181 KeyCondition(
182 self.0 & Condition::gt(KS::SortKey::NAME, value),
183 PhantomData,
184 )
185 }
186
187 /// Adds a sort key greater-than-or-equal constraint: `SK >= value`.
188 ///
189 /// # Examples
190 ///
191 /// ```
192 /// # use dynamodb_facade::test_fixtures::*;
193 /// use dynamodb_facade::DynamoDBItemOp;
194 ///
195 /// let kc = Enrollment::key_condition("user-1").sk_ge("ENROLL#2024");
196 /// assert_eq!(format!("{kc}"), r#"(PK = S("USER#user-1") AND SK >= S("ENROLL#2024"))"#);
197 /// ```
198 pub fn sk_ge(self, value: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
199 KeyCondition(
200 self.0 & Condition::ge(KS::SortKey::NAME, value),
201 PhantomData,
202 )
203 }
204
205 /// Adds a sort key range constraint: `SK BETWEEN low AND high` (inclusive).
206 ///
207 /// # Examples
208 ///
209 /// ```
210 /// # use dynamodb_facade::test_fixtures::*;
211 /// use dynamodb_facade::DynamoDBItemOp;
212 ///
213 /// let kc = Enrollment::key_condition("user-1").sk_between("ENROLL#2024-01", "ENROLL#2024-12");
214 /// assert_eq!(
215 /// format!("{kc}"),
216 /// r#"(PK = S("USER#user-1") AND SK BETWEEN S("ENROLL#2024-01") AND S("ENROLL#2024-12"))"#,
217 /// );
218 /// ```
219 pub fn sk_between(
220 self,
221 low: impl IntoAttributeValue,
222 high: impl IntoAttributeValue,
223 ) -> KeyCondition<'a, KS, WithSk> {
224 KeyCondition(
225 self.0 & Condition::between(KS::SortKey::NAME, low, high),
226 PhantomData,
227 )
228 }
229
230 /// Adds a sort key prefix constraint: `begins_with(SK, prefix)`.
231 ///
232 /// This is the most common sort key condition for hierarchical single-table
233 /// designs, where sort keys are prefixed by entity type (e.g. `"ENROLL#"`).
234 ///
235 /// # Examples
236 ///
237 /// ```
238 /// # use dynamodb_facade::test_fixtures::*;
239 /// use dynamodb_facade::DynamoDBItemOp;
240 ///
241 /// let kc = Enrollment::key_condition("user-1").sk_begins_with("ENROLL#2025");
242 /// assert_eq!(
243 /// format!("{kc}"),
244 /// r#"(PK = S("USER#user-1") AND begins_with(SK, S("ENROLL#2025")))"#,
245 /// );
246 /// ```
247 pub fn sk_begins_with(self, prefix: impl IntoAttributeValue) -> KeyCondition<'a, KS, WithSk> {
248 KeyCondition(
249 self.0 & Condition::begins_with(KS::SortKey::NAME, prefix),
250 PhantomData,
251 )
252 }
253}
254
255// -- Display ------------------------------------------------------------------
256
257impl<KS: KeySchema, S: KeyConditionState> fmt::Display for KeyCondition<'_, KS, S> {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 self.0.fmt(f)
260 }
261}
262
263// -- ApplyKeyCondition impls --------------------------------------------------
264
265use super::{ApplyKeyCondition, KeyConditionableBuilder};
266
267impl<B: KeyConditionableBuilder, KS: KeySchema, S: KeyConditionState> ApplyKeyCondition<B>
268 for KeyCondition<'_, KS, S>
269{
270 fn apply_key_condition(self, builder: B) -> B {
271 self.0.apply_key_condition(builder)
272 }
273}