grid_sdk/store/
mod.rs

1// Copyright 2018-2021 Cargill Incorporated
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(feature = "postgres")]
16pub mod postgres;
17#[cfg(feature = "sqlite")]
18pub mod sqlite;
19
20use std::str::FromStr;
21
22#[cfg(feature = "diesel")]
23use diesel::r2d2::{ConnectionManager, Pool};
24
25#[cfg(feature = "batch-store")]
26use crate::batches::store::BatchStore;
27use crate::commits::store::CommitStore;
28use crate::error::InternalError;
29#[cfg(feature = "location")]
30use crate::location::store::LocationStore;
31#[cfg(feature = "pike")]
32use crate::pike::store::PikeStore;
33#[cfg(feature = "product")]
34use crate::product::store::ProductStore;
35#[cfg(feature = "purchase-order")]
36use crate::purchase_order::store::PurchaseOrderStore;
37#[cfg(feature = "schema")]
38use crate::schema::store::SchemaStore;
39#[cfg(feature = "track-and-trace")]
40use crate::track_and_trace::store::TrackAndTraceStore;
41
42/// An abstract factory for creating Grid stores backed by the same storage
43pub trait StoreFactory {
44    /// Get a new `CommitStore`
45    fn get_grid_commit_store<'a>(&'a self) -> Box<dyn CommitStore + 'a>;
46    /// Get a new `PikeStore`
47    #[cfg(feature = "pike")]
48    fn get_grid_pike_store<'a>(&'a self) -> Box<dyn PikeStore + 'a>;
49    /// Get a new `LocationStore`
50    #[cfg(feature = "location")]
51    fn get_grid_location_store<'a>(&'a self) -> Box<dyn LocationStore + 'a>;
52    /// Get a new `ProductStore`
53    #[cfg(feature = "product")]
54    fn get_grid_product_store<'a>(&'a self) -> Box<dyn ProductStore + 'a>;
55    /// Get a new `SchemaStore`
56    #[cfg(feature = "schema")]
57    fn get_grid_schema_store<'a>(&'a self) -> Box<dyn SchemaStore + 'a>;
58    /// Get a new `TrackAndTraceStore`
59    #[cfg(feature = "track-and-trace")]
60    fn get_grid_track_and_trace_store<'a>(&'a self) -> Box<dyn TrackAndTraceStore + 'a>;
61    #[cfg(feature = "batch-store")]
62    fn get_batch_store<'a>(&'a self) -> Box<dyn BatchStore + 'a>;
63    #[cfg(feature = "purchase-order")]
64    fn get_grid_purchase_order_store<'a>(&'a self) -> Box<dyn PurchaseOrderStore + 'a>;
65}
66
67pub trait TransactionalStoreFactory: StoreFactory + Send + Sync {
68    fn begin_transaction<'a>(&self) -> Result<Box<dyn InContextStoreFactory<'a>>, InternalError>;
69
70    fn clone_box(&self) -> Box<dyn TransactionalStoreFactory>;
71}
72
73pub trait InContextStoreFactory<'a>: StoreFactory {
74    fn commit(&self) -> Result<(), InternalError>;
75
76    fn rollback(&self) -> Result<(), InternalError>;
77}
78
79/// Creates a `StoreFactory` backed by the given connection
80///
81/// # Arguments
82///
83/// * `connection_uri` - The identifier of the storage connection that will be used by all stores
84///   created by the resulting factory
85pub fn create_store_factory(
86    connection_uri: &ConnectionUri,
87) -> Result<Box<dyn TransactionalStoreFactory>, InternalError> {
88    // disable clippy warning caused for some combinations of features
89    // this warning is intended to reduce "needless complexity" but
90    // adding blocks for every combination has the opposite effect
91    #[allow(clippy::match_single_binding)]
92    match connection_uri {
93        #[cfg(feature = "postgres")]
94        ConnectionUri::Postgres(url) => {
95            let connection_manager = ConnectionManager::<diesel::pg::PgConnection>::new(url);
96            let pool = Pool::builder().build(connection_manager).map_err(|err| {
97                InternalError::from_source_with_prefix(
98                    Box::new(err),
99                    "Failed to build connection pool".to_string(),
100                )
101            })?;
102            Ok(Box::new(postgres::PgStoreFactory::new(pool)))
103        }
104        #[cfg(feature = "sqlite")]
105        ConnectionUri::Sqlite(conn_str) => {
106            let connection_manager =
107                ConnectionManager::<diesel::sqlite::SqliteConnection>::new(conn_str);
108            let mut pool_builder = Pool::builder();
109            // A new database is created for each connection to the in-memory SQLite
110            // implementation; to ensure that the resulting stores will operate on the same
111            // database, only one connection is allowed.
112            if conn_str == ":memory:" {
113                pool_builder = pool_builder.max_size(1);
114            }
115            let pool = pool_builder.build(connection_manager).map_err(|err| {
116                InternalError::from_source_with_prefix(
117                    Box::new(err),
118                    "Failed to build connection pool".to_string(),
119                )
120            })?;
121            Ok(Box::new(sqlite::SqliteStoreFactory::new(pool)))
122        }
123        #[cfg(all(not(feature = "sqlite"), not(feature = "postgres")))]
124        _ => Err(InternalError::with_message(
125            "No valid database connection URI".to_string(),
126        )),
127    }
128}
129
130/// The possible connection types and identifiers for a `StoreFactory`
131#[derive(Clone)]
132pub enum ConnectionUri {
133    #[cfg(feature = "postgres")]
134    Postgres(String),
135    #[cfg(feature = "sqlite")]
136    Sqlite(String),
137}
138
139impl FromStr for ConnectionUri {
140    type Err = InternalError;
141
142    // disable clippy warning caused for some combinations of features
143    // this warning is intended to reduce "needless complexity" but
144    // adding blocks for every combination has the opposite effect
145    #[allow(clippy::match_single_binding)]
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        match s {
148            #[cfg(feature = "postgres")]
149            _ if s.starts_with("postgres://") => Ok(ConnectionUri::Postgres(s.into())),
150            #[cfg(feature = "sqlite")]
151            _ => Ok(ConnectionUri::Sqlite(s.into())),
152            #[cfg(not(feature = "sqlite"))]
153            _ => Err(InternalError::with_message(format!(
154                "No compatible connection type: {}",
155                s
156            ))),
157        }
158    }
159}