1use crate::bulk_write_strategy::BulkWriteStrategy;
2use crate::command::Command;
3use crate::connection::Connection;
4use crate::crud_repository_builder::{write_statements, CRUDRepositoryBuilder};
5use crate::crud_repository_options::CRUDRepositoryOptions;
6use crate::datasource_error::DatasourceError;
7use crate::IWhere;
8use crate::parameter::Parameter;
9use crate::parameter_direction::ParameterDirection;
10use crate::response::Response;
11use crate::result::Result;
12use crate::row::Row;
13use crate::value::Value;
14use crate::ExecuteStoredProcedureStatement;
15use async_trait::async_trait;
16use base64::Engine;
17use chrono::{DateTime, Utc};
18use rust_decimal::Decimal;
19use std::collections::BTreeMap;
20use std::sync::{Arc, OnceLock};
21use uuid::Uuid;
22
23pub struct StoredProcedureBulkWriteStrategy {
24 create: String,
25 update: String,
26 delete: String,
27 command: OnceLock<Arc<dyn Command>>,
28 options: OnceLock<CRUDRepositoryOptions>,
29 connection: OnceLock<Arc<dyn Connection>>,
30}
31
32impl StoredProcedureBulkWriteStrategy {
33 pub fn new(
34 create: impl Into<String>,
35 update: impl Into<String>,
36 delete: impl Into<String>,
37 ) -> Self {
38 Self {
39 create: create.into(),
40 update: update.into(),
41 delete: delete.into(),
42 command: OnceLock::new(),
43 options: OnceLock::new(),
44 connection: OnceLock::new(),
45 }
46 }
47
48 fn command(&self) -> Result<Arc<dyn Command>> {
49 self.command
50 .get()
51 .cloned()
52 .ok_or_else(|| DatasourceError::message("Command required"))
53 }
54
55 fn options(&self) -> Result<&CRUDRepositoryOptions> {
56 self.options
57 .get()
58 .ok_or_else(|| DatasourceError::message("Options required"))
59 }
60
61 fn reverse(&self, item: &Row) -> Result<Row> {
62 let options = self.options()?;
63 let row = match &options.object_converter {
64 Some(converter) => converter.reverse_convert(item)?,
65 None => item.clone(),
66 };
67 match &options.row_converter {
68 Some(converter) => converter.reverse_convert(&row),
69 None => Ok(row),
70 }
71 }
72
73 fn column(&self, property: &str) -> String {
74 self.options()
75 .ok()
76 .and_then(|options| options.property_mappings.as_ref())
77 .map(|mapper| mapper.map(property.to_string()))
78 .unwrap_or_else(|| property.to_string())
79 }
80
81 fn row_of(&self, item: &Row, create: bool) -> Result<Row> {
82 let stored = self.reverse(item)?;
83 let connection = self
84 .connection
85 .get()
86 .cloned()
87 .ok_or_else(|| DatasourceError::message("Connection required"))?;
88 let built = CRUDRepositoryBuilder::parts(connection, self.options()?.clone())?;
89 let statements = write_statements(&built.statements);
90 let mut row = Row::new();
91 for name in statements.read_columns() {
92 let physical = self.column(name);
93 if create && self.identity(name) && !assigned(stored.get(name).or_else(|| stored.get(self.ids()[0].as_str()))) {
94 continue;
95 }
96 let value = stored
97 .iter()
98 .find(|(key, _)| *key == name || self.column(key) == physical)
99 .map(|(_, value)| value.clone())
100 .unwrap_or(Value::Null);
101 row.insert(physical, value);
102 }
103 Ok(row)
104 }
105
106 fn identity(&self, column: &str) -> bool {
107 self.ids().len() == 1 && self.ids()[0] == column
108 }
109
110 async fn call(&self, name: &str, items: &[Row]) -> Result<Response> {
111 self.call_with(name, items, false).await
112 }
113
114 async fn call_with(&self, name: &str, items: &[Row], generated: bool) -> Result<Response> {
115 if items.is_empty() {
116 return Ok(Response::new(Value::Null, 0, None));
117 }
118 let rows = items
119 .iter()
120 .map(|item| self.row_of(item, generated))
121 .collect::<Result<Vec<_>>>()?;
122 let payload = Parameter::new("payload", json(&rows));
123 let parameters = if generated && self.ids().len() == 1 {
124 vec![
125 payload,
126 Parameter::with_direction("ids", Value::Null, ParameterDirection::InputOutput),
127 ]
128 } else {
129 vec![payload]
130 };
131 let statement = ExecuteStoredProcedureStatement::new(name, ¶meters);
132 let response = self
133 .command()?
134 .execute_statement(&statement, Some(¶meters))
135 .await?;
136 Ok(response)
137 }
138
139 fn ids(&self) -> &[String] {
140 self.options
141 .get()
142 .map(|options| options.ids.as_slice())
143 .unwrap_or(&[])
144 }
145
146 fn apply_ids(&self, items: &mut [Row], parameters: Option<&[Parameter]>) {
147 if self.ids().len() != 1 {
148 return;
149 }
150 let found = parameters.and_then(|list| list.iter().find(|parameter| parameter.name == "ids"));
151 let ids = parse_ids(found.map(|parameter| ¶meter.value));
152 let column = self.ids()[0].clone();
153 for (item, id) in items.iter_mut().zip(ids) {
154 item.insert(column.clone(), Value::Int(id));
155 }
156 }
157}
158
159#[async_trait]
160impl BulkWriteStrategy for StoredProcedureBulkWriteStrategy {
161 fn attach(
162 &self,
163 command: Arc<dyn Command>,
164 options: CRUDRepositoryOptions,
165 connection: Arc<dyn Connection>,
166 ) {
167 let _ = self.command.set(command);
168 let _ = self.options.set(options);
169 let _ = self.connection.set(connection);
170 }
171
172 fn persist_children(&self) -> bool {
173 true
174 }
175
176 async fn create(&self, items: &mut [Row]) -> Result<Response> {
177 let response = self.call_with(&self.create, items, true).await?;
178 self.apply_ids(items, response.parameters.as_deref());
179 Ok(response)
180 }
181
182 async fn update(&self, items: &[Row], _by_where: Option<&dyn IWhere>) -> Result<Response> {
183 self.call(&self.update, items).await
184 }
185
186 async fn delete(&self, items: &[Row], _by_where: Option<&dyn IWhere>) -> Result<Response> {
187 self.call(&self.delete, items).await
188 }
189}
190
191fn assigned(value: Option<&Value>) -> bool {
192 match value {
193 None | Some(Value::Null) => false,
194 Some(Value::Int(0)) => false,
195 Some(Value::Text(text)) => !text.is_empty(),
196 Some(_) => true,
197 }
198}
199
200fn parse_ids(value: Option<&Value>) -> Vec<i64> {
201 let text = match value {
202 Some(Value::Text(text)) => text.trim(),
203 Some(Value::Int(number)) => return vec![*number],
204 Some(Value::Binary(bytes)) => std::str::from_utf8(bytes).unwrap_or("").trim(),
205 _ => return vec![],
206 };
207 if text.is_empty() || text == "[]" || text == "null" {
208 return vec![];
209 }
210 text.trim_start_matches('[')
211 .trim_end_matches(']')
212 .split(',')
213 .filter_map(|part| part.trim().parse().ok())
214 .collect()
215}
216
217fn json(rows: &[Row]) -> String {
218 let objects: Vec<String> = rows.iter().map(object).collect();
219 format!("[{}]", objects.join(","))
220}
221
222fn object(row: &Row) -> String {
223 let fields: BTreeMap<&str, &Value> = row
224 .iter()
225 .map(|(key, value)| (key.as_str(), value))
226 .collect();
227 let body: Vec<String> = fields
228 .into_iter()
229 .map(|(key, value)| format!("{}:{}", quote(key), scalar(value)))
230 .collect();
231 format!("{{{}}}", body.join(","))
232}
233
234fn scalar(value: &Value) -> String {
235 match value {
236 Value::Null => "null".to_string(),
237 Value::Bool(flag) => if *flag { "true" } else { "false" }.to_string(),
238 Value::Int(number) => number.to_string(),
239 Value::Float(number) => number.to_string(),
240 Value::Text(text) => quote(text),
241 Value::DateTime(date) => quote(&round_trip(*date)),
242 Value::Uuid(id) => quote(&uuid_text(id)),
243 Value::Decimal(number) => quote(&decimal_text(number)),
244 Value::Binary(bytes) => quote(&base64::engine::general_purpose::STANDARD.encode(bytes)),
245 }
246}
247
248fn round_trip(date: DateTime<Utc>) -> String {
249 format!(
250 "{}.{:07}Z",
251 date.format("%Y-%m-%dT%H:%M:%S"),
252 date.timestamp_subsec_nanos() / 100
253 )
254}
255
256fn uuid_text(id: &Uuid) -> String {
257 id.to_string()
258}
259
260fn decimal_text(number: &Decimal) -> String {
261 number.to_string()
262}
263
264fn quote(text: &str) -> String {
265 let mut result = String::from("\"");
266 for ch in text.chars() {
267 match ch {
268 '\\' => result.push_str("\\\\"),
269 '"' => result.push_str("\\\""),
270 '\u{0008}' => result.push_str("\\b"),
271 '\u{000c}' => result.push_str("\\f"),
272 '\n' => result.push_str("\\n"),
273 '\r' => result.push_str("\\r"),
274 '\t' => result.push_str("\\t"),
275 ch if (ch as u32) < 0x20 => result.push_str(&format!("\\u{:04x}", ch as u32)),
276 ch => result.push(ch),
277 }
278 }
279 result.push('"');
280 result
281}