Skip to main content

modyne/
keys.rs

1//! Types representing DynamoDB keys in a single-table design
2//!
3//! # Working with Local Secondary Indexes
4//!
5//! Because the partition key on an LSI be the same as the partition
6//! key on the table, it _may_ be omitted when constructing the full set
7//! of key attributes for a put or update operation. There is no danger
8//! in including it, but it will be overriden by the table's partition
9//! key.
10//!
11//! However, when used for a query or scan operation, the partition key
12//! must be provided.
13//!
14//! # Example
15//!
16//! Constructing the key for an LSI as part of a put operation:
17//!
18//! ```
19//! use modyne::keys;
20//!
21//! let primary = keys::Primary {
22//!    hash: "PART#ABCD".to_string(),
23//!    range: "SORT#1234".to_string(),
24//! };
25//! let lsi = keys::Lsi1 {
26//!     hash: String::default(),
27//!     range: "LSI1#9876".to_string(),
28//! };
29//! let full_key = keys::FullKey { primary, indexes: lsi }.into_key();
30//!
31//! assert_eq!(full_key["PK"].as_s().unwrap(), "PART#ABCD");
32//! assert_eq!(full_key["SK"].as_s().unwrap(), "SORT#1234");
33//! assert_eq!(full_key["LSI1SK"].as_s().unwrap(), "LSI1#9876");
34//! ```
35//!
36//! Constructing the key for an LSI as part of a query operation:
37//!
38//! ```
39//! use modyne::keys::{IndexKeys, Lsi1};
40//!
41//! let lsi = Lsi1 {
42//!     hash: "PART#ABCD".to_string(),
43//!     range: "LSI1#9876".to_string(),
44//! };
45//! let full_key = lsi.into_key();
46//!
47//! assert_eq!(full_key["PK"].as_s().unwrap(), "PART#ABCD");
48//! assert_eq!(full_key["LSI1SK"].as_s().unwrap(), "LSI1#9876");
49//! ```
50
51use crate::Item;
52
53/// A DynamoDB key
54pub trait Key: Sized + serde::Serialize {
55    /// The core properties of the key, determining how data is stored and accessed
56    const DEFINITION: KeyDefinition;
57}
58
59/// A set of keys used as secondary indexes
60pub trait IndexKeys: Sized {
61    /// The definitions for the keys
62    const KEY_DEFINITIONS: &'static [SecondaryIndexDefinition];
63
64    /// The intermediate type used to serialize the key
65    type Serialize<'a>: serde::Serialize
66    where
67        Self: 'a;
68
69    /// Constructs the intermediate type used to serialize the key
70    fn to_serialize(&self) -> Self::Serialize<'_>;
71
72    /// Converts the key into a DynamoDB item
73    fn into_key(self) -> Item {
74        crate::codec::to_item(self.to_serialize()).unwrap()
75    }
76}
77
78/// A DynamoDB primary key
79pub trait PrimaryKey: Sized + serde::Serialize {
80    /// The definition for the primary key
81    const PRIMARY_KEY_DEFINITION: PrimaryKeyDefinition;
82
83    /// Converts the key into a DynamoDB item
84    fn into_key(self) -> Item {
85        crate::codec::to_item(self).unwrap()
86    }
87}
88
89/// The primary key for a DynamoDB table
90#[derive(Clone, Debug, serde::Serialize)]
91pub struct Primary {
92    /// The partition key, with attribute name `PK`
93    #[serde(rename = "PK")]
94    pub hash: String,
95
96    /// The sort key, with attribute name `SK`
97    #[serde(rename = "SK")]
98    pub range: String,
99}
100
101impl PrimaryKey for Primary {
102    const PRIMARY_KEY_DEFINITION: PrimaryKeyDefinition = PrimaryKeyDefinition {
103        hash_key: "PK",
104        range_key: Some("SK"),
105    };
106}
107
108impl Key for Primary {
109    const DEFINITION: KeyDefinition = KeyDefinition::Primary(Self::PRIMARY_KEY_DEFINITION);
110}
111
112/// A DynamoDB secondary index key
113pub trait IndexKey: Sized + serde::Serialize {
114    /// The definition for the index
115    const INDEX_DEFINITION: SecondaryIndexDefinition;
116}
117
118impl<K: IndexKey> Key for K {
119    const DEFINITION: KeyDefinition = KeyDefinition::Secondary(K::INDEX_DEFINITION);
120}
121
122impl<K: IndexKey> IndexKey for Option<K> {
123    const INDEX_DEFINITION: SecondaryIndexDefinition = K::INDEX_DEFINITION;
124}
125
126/// The primary key for an item along with the relevant secondary index keys
127#[derive(Clone, Debug, serde::Serialize)]
128pub struct FullKey<P, I>
129where
130    P: PrimaryKey,
131    I: IndexKeys,
132{
133    /// The secondary index keys relavant to the item
134    #[serde(flatten, serialize_with = "serialize_keys")]
135    pub indexes: I,
136
137    /// The primary key for the item
138    #[serde(flatten)]
139    pub primary: P,
140}
141
142impl<P, I> FullKey<P, I>
143where
144    P: PrimaryKey,
145    I: IndexKeys,
146{
147    /// Converts the key into a DynamoDB item
148    pub fn into_key(self) -> Item {
149        crate::codec::to_item(self).unwrap()
150    }
151}
152
153impl<P> From<P> for FullKey<P, ()>
154where
155    P: PrimaryKey,
156{
157    #[inline]
158    fn from(primary: P) -> Self {
159        Self {
160            indexes: (),
161            primary,
162        }
163    }
164}
165
166fn serialize_keys<K, S>(keys: &K, serializer: S) -> Result<S::Ok, S::Error>
167where
168    K: IndexKeys,
169    S: serde::Serializer,
170{
171    serde::Serialize::serialize(&keys.to_serialize(), serializer)
172}
173
174macro_rules! gsi_key {
175    ($name:ident: $idx:literal, $pk:literal, $sk:literal) => {
176        /// The key for a global secondary index
177        #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, serde::Serialize)]
178        pub struct $name {
179            #[doc = "The partition key, with attribute name `"]
180            #[doc = $pk]
181            #[doc = "`"]
182            #[serde(rename = $pk)]
183            pub hash: String,
184
185            #[doc = "The sort key, with attribute name `"]
186            #[doc = $pk]
187            #[doc = "`"]
188            #[serde(rename = $sk)]
189            pub range: String,
190        }
191
192        impl IndexKey for $name {
193            const INDEX_DEFINITION: SecondaryIndexDefinition =
194                SecondaryIndexDefinition::Global(GlobalSecondaryIndexDefinition {
195                    index_name: $idx,
196                    hash_key: $pk,
197                    range_key: Some($sk),
198                });
199        }
200    };
201}
202
203gsi_key!(Gsi1: "GSI1", "GSI1PK", "GSI1SK");
204gsi_key!(Gsi2: "GSI2", "GSI2PK", "GSI2SK");
205gsi_key!(Gsi3: "GSI3", "GSI3PK", "GSI3SK");
206gsi_key!(Gsi4: "GSI4", "GSI4PK", "GSI4SK");
207gsi_key!(Gsi5: "GSI5", "GSI5PK", "GSI5SK");
208gsi_key!(Gsi6: "GSI6", "GSI6PK", "GSI6SK");
209gsi_key!(Gsi7: "GSI7", "GSI7PK", "GSI7SK");
210gsi_key!(Gsi8: "GSI8", "GSI8PK", "GSI8SK");
211gsi_key!(Gsi9: "GSI9", "GSI9PK", "GSI9SK");
212gsi_key!(Gsi10: "GSI10", "GSI10PK", "GSI10SK");
213gsi_key!(Gsi11: "GSI11", "GSI11PK", "GSI11SK");
214gsi_key!(Gsi12: "GSI12", "GSI12PK", "GSI12SK");
215gsi_key!(Gsi13: "GSI13", "GSI13PK", "GSI13SK");
216gsi_key!(Gsi14: "GSI14", "GSI14PK", "GSI14SK");
217gsi_key!(Gsi15: "GSI15", "GSI15PK", "GSI15SK");
218gsi_key!(Gsi16: "GSI16", "GSI16PK", "GSI16SK");
219gsi_key!(Gsi17: "GSI17", "GSI17PK", "GSI17SK");
220gsi_key!(Gsi18: "GSI18", "GSI18PK", "GSI18SK");
221gsi_key!(Gsi19: "GSI19", "GSI19PK", "GSI19SK");
222gsi_key!(Gsi20: "GSI20", "GSI20PK", "GSI20SK");
223
224macro_rules! lsi_key {
225    ($name:ident: $idx:literal, $sk:literal) => {
226        /// The key for a local secondary index
227        ///
228        /// See the [module documentation][crate::keys#Working_with_Local_Secondary_Indexes]
229        /// for more information on how to use this type.
230        #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, serde::Serialize)]
231        pub struct $name {
232            /// The partition key for the table, with attribute name `PK`
233            #[serde(rename = "PK")]
234            pub hash: String,
235
236            #[doc = "The sort key for the local secondary index, with attribute name `"]
237            #[doc = $sk]
238            #[doc = "`"]
239            #[serde(rename = $sk)]
240            pub range: String,
241        }
242
243        impl IndexKey for $name {
244            const INDEX_DEFINITION: SecondaryIndexDefinition =
245                SecondaryIndexDefinition::Local(LocalSecondaryIndexDefinition {
246                    index_name: $idx,
247                    hash_key: "PK",
248                    range_key: $sk,
249                });
250        }
251    };
252}
253
254lsi_key!(Lsi1: "LSI1", "LSI1SK");
255lsi_key!(Lsi2: "LSI2", "LSI2SK");
256lsi_key!(Lsi3: "LSI3", "LSI3SK");
257lsi_key!(Lsi4: "LSI4", "LSI4SK");
258lsi_key!(Lsi5: "LSI5", "LSI5SK");
259
260macro_rules! impl_key_tuples {
261    ($i:ident; $($n:tt : $ty:ident),*$(,)?) => {
262        /// A composite serialization of multiple keys
263        #[derive(Debug, serde::Serialize)]
264        #[allow(non_snake_case)]
265        pub struct $i<'a, $($ty),*> {
266            $(#[serde(flatten)] $ty: &'a $ty,)*
267        }
268
269        impl<$($ty: IndexKey),*> IndexKeys for ($($ty,)*)
270        where
271            $(
272                for<'a> $ty: 'a,
273            )*
274        {
275            const KEY_DEFINITIONS: &'static [$crate::keys::SecondaryIndexDefinition] = &[
276                $(
277                    $ty::INDEX_DEFINITION,
278                )*
279            ];
280            type Serialize<'a> = $i<'a, $($ty),*>;
281            #[inline]
282            fn to_serialize(&self) -> Self::Serialize<'_> {
283                $i {
284                    $($ty: &self.$n,)*
285                }
286            }
287        }
288    };
289}
290
291impl<T: IndexKey> IndexKeys for T {
292    const KEY_DEFINITIONS: &'static [SecondaryIndexDefinition] = &[T::INDEX_DEFINITION];
293    type Serialize<'a> = &'a T
294    where
295        T: 'a;
296    #[inline]
297    fn to_serialize(&self) -> Self::Serialize<'_> {
298        self
299    }
300}
301
302impl<K: Key> crate::ScanInput for K {
303    type Index = K;
304}
305
306mod hidden {
307    #[derive(Debug, serde::Serialize)]
308    pub struct Empty {}
309}
310
311impl IndexKeys for () {
312    const KEY_DEFINITIONS: &'static [SecondaryIndexDefinition] = &[];
313    type Serialize<'a> = hidden::Empty;
314    #[inline]
315    fn to_serialize(&self) -> Self::Serialize<'_> {
316        hidden::Empty {}
317    }
318}
319
320mod composite_keys {
321    use super::*;
322    impl_key_tuples! { CompositeK0; 0: K0 }
323    impl_key_tuples! { CompositeK1; 0: K0, 1: K1 }
324    impl_key_tuples! { CompositeK2; 0: K0, 1: K1, 2: K2 }
325    impl_key_tuples! { CompositeK3; 0: K0, 1: K1, 2: K2, 3: K3 }
326    impl_key_tuples! { CompositeK4; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4 }
327    impl_key_tuples! { CompositeK5; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5 }
328    impl_key_tuples! { CompositeK6; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6 }
329    impl_key_tuples! { CompositeK7; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7 }
330    impl_key_tuples! { CompositeK8; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7, 8: K8 }
331    impl_key_tuples! { CompositeK9; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7, 8: K8, 9: K9 }
332    impl_key_tuples! { CompositeK10; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7, 8: K8, 9: K9, 10: K10 }
333    impl_key_tuples! { CompositeK11; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7, 8: K8, 9: K9, 10: K10, 11: K11 }
334    impl_key_tuples! { CompositeK12; 0: K0, 1: K1, 2: K2, 3: K3, 4: K4, 5: K5, 6: K6, 7: K7, 8: K8, 9: K9, 10: K10, 11: K11, 12: K12 }
335}
336
337/// A key definition
338#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
339pub enum KeyDefinition {
340    /// The primary key
341    Primary(PrimaryKeyDefinition),
342
343    /// A secondary index
344    Secondary(SecondaryIndexDefinition),
345}
346
347impl KeyDefinition {
348    /// The name of the index, if any
349    #[inline]
350    pub const fn index_name(&self) -> Option<&'static str> {
351        match self {
352            Self::Primary(_) => None,
353            Self::Secondary(def) => Some(def.index_name()),
354        }
355    }
356
357    /// The hash key
358    #[inline]
359    pub const fn hash_key(&self) -> &'static str {
360        match self {
361            Self::Primary(def) => def.hash_key,
362            Self::Secondary(def) => def.hash_key(),
363        }
364    }
365
366    /// The range key, if any
367    #[inline]
368    pub const fn range_key(&self) -> Option<&'static str> {
369        match self {
370            Self::Primary(def) => def.range_key,
371            Self::Secondary(def) => def.range_key(),
372        }
373    }
374}
375
376impl From<PrimaryKeyDefinition> for KeyDefinition {
377    #[inline]
378    fn from(def: PrimaryKeyDefinition) -> Self {
379        Self::Primary(def)
380    }
381}
382
383impl From<SecondaryIndexDefinition> for KeyDefinition {
384    #[inline]
385    fn from(def: SecondaryIndexDefinition) -> Self {
386        Self::Secondary(def)
387    }
388}
389
390/// A primary key definition
391#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
392pub struct PrimaryKeyDefinition {
393    /// The hash key
394    pub hash_key: &'static str,
395
396    /// The range key, if any
397    pub range_key: Option<&'static str>,
398}
399
400impl PrimaryKeyDefinition {
401    /// Convert into a key definition
402    #[inline]
403    pub const fn into_key_definition(self) -> KeyDefinition {
404        KeyDefinition::Primary(self)
405    }
406}
407
408/// A secondary index definition
409#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
410pub enum SecondaryIndexDefinition {
411    /// A global secondary index
412    Global(GlobalSecondaryIndexDefinition),
413
414    /// A local secondary index
415    Local(LocalSecondaryIndexDefinition),
416}
417
418impl SecondaryIndexDefinition {
419    /// Get the name of the index
420    #[inline]
421    pub const fn index_name(&self) -> &'static str {
422        match self {
423            Self::Global(def) => def.index_name,
424            Self::Local(def) => def.index_name,
425        }
426    }
427
428    /// Get the hash key of the index
429    #[inline]
430    pub const fn hash_key(&self) -> &'static str {
431        match self {
432            Self::Global(def) => def.hash_key,
433            Self::Local(def) => def.hash_key,
434        }
435    }
436
437    /// Get the range key of the index
438    #[inline]
439    pub const fn range_key(&self) -> Option<&'static str> {
440        match self {
441            Self::Global(def) => def.range_key,
442            Self::Local(def) => Some(def.range_key),
443        }
444    }
445
446    /// Convert into a key definition
447    #[inline]
448    pub const fn into_key_definition(self) -> KeyDefinition {
449        KeyDefinition::Secondary(self)
450    }
451}
452
453/// A global secondary index definition
454#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
455pub struct GlobalSecondaryIndexDefinition {
456    /// The name of the index
457    pub index_name: &'static str,
458
459    /// The hash key of the index
460    pub hash_key: &'static str,
461
462    /// The range key of the index
463    pub range_key: Option<&'static str>,
464}
465
466/// A global secondary index definition
467impl GlobalSecondaryIndexDefinition {
468    /// Convert into a secondary index definition
469    #[inline]
470    pub const fn into_index(self) -> SecondaryIndexDefinition {
471        SecondaryIndexDefinition::Global(self)
472    }
473}
474
475/// A local secondary index definition
476#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
477pub struct LocalSecondaryIndexDefinition {
478    /// The name of the index
479    pub index_name: &'static str,
480
481    /// The hash key of the table
482    ///
483    /// This must match the name of the hash key of the table
484    pub hash_key: &'static str,
485
486    /// The range key of the index
487    pub range_key: &'static str,
488}
489
490/// A local secondary index definition
491impl LocalSecondaryIndexDefinition {
492    /// Convert into a secondary index definition
493    #[inline]
494    pub const fn into_index(self) -> SecondaryIndexDefinition {
495        SecondaryIndexDefinition::Local(self)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use aws_sdk_dynamodb::types::AttributeValue;
502
503    use super::*;
504
505    #[test]
506    fn test_primary_key() {
507        let key = Primary {
508            hash: "hash".to_string(),
509            range: "range".to_string(),
510        };
511        let serialized = key.into_key();
512        assert_eq!(serialized["PK"], AttributeValue::S("hash".to_string()));
513        assert_eq!(serialized["SK"], AttributeValue::S("range".to_string()));
514    }
515
516    #[test]
517    fn test_gsi_key() {
518        let key = Gsi1 {
519            hash: "hash".to_string(),
520            range: "range".to_string(),
521        };
522        let serialized = key.into_key();
523        assert_eq!(serialized["GSI1PK"], AttributeValue::S("hash".to_string()));
524        assert_eq!(serialized["GSI1SK"], AttributeValue::S("range".to_string()));
525    }
526
527    #[test]
528    fn test_lsi_key() {
529        let key = Lsi1 {
530            hash: "primary_key".to_string(),
531            range: "range".to_string(),
532        };
533        let serialized = key.into_key();
534        assert_eq!(
535            serialized["PK"],
536            AttributeValue::S("primary_key".to_string())
537        );
538        assert_eq!(serialized["LSI1SK"], AttributeValue::S("range".to_string()));
539    }
540
541    #[test]
542    fn test_composite_key() {
543        let primary = Primary {
544            hash: "PK".to_string(),
545            range: "SK".to_string(),
546        };
547
548        let gsi5 = Gsi5 {
549            hash: "GSI5PK".to_string(),
550            range: "GSI5SK".to_string(),
551        };
552
553        let lsi3 = Lsi3 {
554            // Note that this _should_ be the same as the primary key's hash, but
555            // we set it to something else to make sure it is overridden once
556            // serialized.
557            hash: "LSI3PK".to_string(),
558            range: "LSI3SK".to_string(),
559        };
560
561        let serialized = FullKey {
562            primary,
563            indexes: (gsi5, lsi3),
564        }
565        .into_key();
566        assert_eq!(serialized["PK"], AttributeValue::S("PK".to_string()));
567        assert_eq!(serialized["SK"], AttributeValue::S("SK".to_string()));
568        assert_eq!(
569            serialized["GSI5PK"],
570            AttributeValue::S("GSI5PK".to_string())
571        );
572        assert_eq!(
573            serialized["GSI5SK"],
574            AttributeValue::S("GSI5SK".to_string())
575        );
576        assert_eq!(
577            serialized["LSI3SK"],
578            AttributeValue::S("LSI3SK".to_string())
579        );
580    }
581}