Skip to main content

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}