1use crate::database::{MEMORY_DB, SQLiteDatabase, get_shared_mem_conn, values_to_sql};
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::generic::GenericError;
4use rusqlite::{Connection, params_from_iter};
5
6#[derive(Clone)]
7pub struct StorInsert;
8
9impl Command for StorInsert {
10 fn name(&self) -> &str {
11 "stor insert"
12 }
13
14 fn signature(&self) -> Signature {
15 Signature::build("stor insert")
16 .input_output_types(vec![
17 (Type::Nothing, Type::table()),
18 (Type::record(), Type::table()),
19 (Type::table(), Type::table()),
20 (Type::Any, Type::table()),
23 ])
24 .required_named(
25 "table-name",
26 SyntaxShape::String,
27 "Name of the table you want to insert into.",
28 Some('t'),
29 )
30 .named(
31 "data-record",
32 SyntaxShape::record(),
33 "A record of column names and column values to insert into the specified table.",
34 Some('d'),
35 )
36 .allow_variants_without_examples(true)
37 .category(Category::Database)
38 }
39
40 fn description(&self) -> &str {
41 "Insert information into a specified table in the in-memory sqlite database."
42 }
43
44 fn search_terms(&self) -> Vec<&str> {
45 vec!["sqlite", "storing", "table", "saving"]
46 }
47
48 fn examples(&self) -> Vec<Example<'_>> {
49 vec![
50 Example {
51 description: "Insert data in the in-memory sqlite database using a data-record of column-name and column-value pairs",
52 example: "stor insert --table-name nudb --data-record {bool1: true, int1: 5, float1: 1.1, str1: fdncred, datetime1: 2023-04-17}",
53 result: None,
54 },
55 Example {
56 description: "Insert data through pipeline input as a record of column-name and column-value pairs",
57 example: "{bool1: true, int1: 5, float1: 1.1, str1: fdncred, datetime1: 2023-04-17} | stor insert --table-name nudb",
58 result: None,
59 },
60 Example {
61 description: "Insert data through pipeline input as a table literal",
62 example: "[[bool1 int1 float1]; [true 5 1.1], [false 8 3.14]] | stor insert --table-name nudb",
63 result: None,
64 },
65 Example {
66 description: "Insert ls entries",
67 example: "ls | stor insert --table-name files",
68 result: None,
69 },
70 Example {
71 description: "Insert nu records as json data",
72 example: "ls -l | each {{file: $in.name, metadata: ($in | reject name)}} | stor insert --table-name files_with_md",
73 result: None,
74 },
75 ]
76 }
77
78 fn run(
79 &self,
80 engine_state: &EngineState,
81 stack: &mut Stack,
82 call: &Call,
83 input: PipelineData,
84 ) -> Result<PipelineData, ShellError> {
85 let span = call.head;
86 let table_name: Option<String> = call.get_flag(engine_state, stack, "table-name")?;
87 let data_record: Option<Record> = call.get_flag(engine_state, stack, "data-record")?;
88
89 let records = handle(span, data_record, input)?;
90
91 let conn = get_shared_mem_conn()?;
92 for record in records {
93 process(engine_state, table_name.clone(), span, &conn, record)?;
94 }
95
96 let db = Box::new(SQLiteDatabase::new(
97 std::path::Path::new(MEMORY_DB),
98 engine_state.signals().clone(),
99 ));
100 Ok(Value::custom(db, span).into_pipeline_data())
101 }
102}
103
104fn handle(
105 span: Span,
106 data_record: Option<Record>,
107 input: PipelineData,
108) -> Result<Vec<Record>, ShellError> {
109 if let Some(record) = data_record {
111 if !matches!(input, PipelineData::Empty) {
112 return Err(ShellError::Generic(GenericError::new(
113 "Pipeline and Flag both being used",
114 "Use either pipeline input or '--data-record' parameter",
115 span,
116 )));
117 }
118 return Ok(vec![record]);
119 }
120
121 let values = match input {
123 PipelineData::Empty => {
124 return Err(ShellError::MissingParameter {
125 param_name: "requires a table or a record".into(),
126 span,
127 });
128 }
129 PipelineData::ListStream(stream, ..) => stream.into_iter().collect::<Vec<_>>(),
130 PipelineData::Value(Value::List { vals, .. }, ..) => vals.into_owned(),
131 PipelineData::Value(val, ..) => vec![val],
132 _ => {
133 return Err(ShellError::OnlySupportsThisInputType {
134 exp_input_type: "list or record".into(),
135 wrong_type: "".into(),
136 dst_span: span,
137 src_span: span,
138 });
139 }
140 };
141
142 values
143 .into_iter()
144 .map(|val| match val {
145 Value::Record { val, .. } => Ok(val.into_owned()),
146 other => Err(ShellError::OnlySupportsThisInputType {
147 exp_input_type: "record".into(),
148 wrong_type: other.get_type().to_string(),
149 dst_span: span,
150 src_span: other.span(),
151 }),
152 })
153 .collect()
154}
155
156fn process(
157 engine_state: &EngineState,
158 table_name: Option<String>,
159 span: Span,
160 conn: &Connection,
161 record: Record,
162) -> Result<(), ShellError> {
163 if table_name.is_none() {
164 return Err(ShellError::MissingParameter {
165 param_name: "requires at table name".into(),
166 span,
167 });
168 }
169 let new_table_name = table_name.unwrap_or("table".into());
170
171 let mut create_stmt = format!("INSERT INTO {new_table_name} (");
172 let mut column_placeholders: Vec<String> = Vec::new();
173
174 let cols = record.columns();
175 cols.for_each(|col| {
176 column_placeholders.push(col.to_string());
177 });
178
179 create_stmt.push_str(&column_placeholders.join(", "));
180
181 create_stmt.push_str(") VALUES (");
183 let mut value_placeholders: Vec<String> = Vec::new();
184 for (index, _) in record.columns().enumerate() {
185 value_placeholders.push(format!("?{}", index + 1));
186 }
187 create_stmt.push_str(&value_placeholders.join(", "));
188 create_stmt.push(')');
189
190 let params = values_to_sql(engine_state, record.values().cloned(), span)?;
194
195 conn.execute(&create_stmt, params_from_iter(params))
196 .map_err(|err| {
197 ShellError::Generic(GenericError::new_internal(
198 "Failed to insert using the SQLite connection to the in-memory database from insert.rs.",
199 err.to_string(),
200 ))
201 })?;
202 Ok(())
203}
204
205#[cfg(test)]
206mod test {
207 use chrono::DateTime;
208
209 use super::*;
210
211 #[test]
212 fn test_examples() -> nu_test_support::Result {
213 nu_test_support::test().examples(StorInsert)
214 }
215
216 #[test]
217 fn test_process_with_simple_parameters() {
218 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
219 let create_stmt = "CREATE TABLE test_process_with_simple_parameters (
220 int_column INTEGER,
221 real_column REAL,
222 str_column VARCHAR(255),
223 bool_column BOOLEAN,
224 date_column DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
225 )";
226
227 conn.execute(create_stmt, [])
228 .expect("Failed to create table as part of test.");
229 let table_name = Some("test_process_with_simple_parameters".to_string());
230 let span = Span::test_data();
231 let mut columns = Record::new();
232 columns.insert("int_column".to_string(), Value::test_int(42));
233 columns.insert("real_column".to_string(), Value::test_float(3.1));
234 columns.insert(
235 "str_column".to_string(),
236 Value::test_string("SimpleString".to_string()),
237 );
238 columns.insert("bool_column".to_string(), Value::test_bool(true));
239 columns.insert(
240 "date_column".to_string(),
241 Value::test_date(
242 DateTime::parse_from_str("2021-12-30 00:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
243 .expect("Date string should parse."),
244 ),
245 );
246
247 let result = process(&EngineState::new(), table_name, span, &conn, columns);
248
249 assert!(result.is_ok());
250 }
251
252 #[test]
253 fn test_process_string_with_space() {
254 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
255 let create_stmt = "CREATE TABLE test_process_string_with_space (
256 str_column VARCHAR(255)
257 )";
258
259 conn.execute(create_stmt, [])
260 .expect("Failed to create table as part of test.");
261 let table_name = Some("test_process_string_with_space".to_string());
262 let span = Span::test_data();
263 let mut columns = Record::new();
264 columns.insert(
265 "str_column".to_string(),
266 Value::test_string("String With Spaces".to_string()),
267 );
268
269 let result = process(&EngineState::new(), table_name, span, &conn, columns);
270
271 assert!(result.is_ok());
272 }
273
274 #[test]
275 fn test_no_errors_when_string_too_long() {
276 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
277 let create_stmt = "CREATE TABLE test_errors_when_string_too_long (
278 str_column VARCHAR(8)
279 )";
280
281 conn.execute(create_stmt, [])
282 .expect("Failed to create table as part of test.");
283 let table_name = Some("test_errors_when_string_too_long".to_string());
284 let span = Span::test_data();
285 let mut columns = Record::new();
286 columns.insert(
287 "str_column".to_string(),
288 Value::test_string("ThisIsALongString".to_string()),
289 );
290
291 let result = process(&EngineState::new(), table_name, span, &conn, columns);
292 assert!(result.is_ok());
294 }
295
296 #[test]
297 fn test_no_errors_when_param_is_wrong_type() {
298 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
299 let create_stmt = "CREATE TABLE test_errors_when_param_is_wrong_type (
300 int_column INT
301 )";
302
303 conn.execute(create_stmt, [])
304 .expect("Failed to create table as part of test.");
305 let table_name = Some("test_errors_when_param_is_wrong_type".to_string());
306 let span = Span::test_data();
307 let mut columns = Record::new();
308 columns.insert(
309 "int_column".to_string(),
310 Value::test_string("ThisIsTheWrongType".to_string()),
311 );
312
313 let result = process(&EngineState::new(), table_name, span, &conn, columns);
314 assert!(result.is_ok());
316 }
317
318 #[test]
319 fn test_errors_when_column_doesnt_exist() {
320 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
321 let create_stmt = "CREATE TABLE test_errors_when_column_doesnt_exist (
322 int_column INT
323 )";
324
325 conn.execute(create_stmt, [])
326 .expect("Failed to create table as part of test.");
327 let table_name = Some("test_errors_when_column_doesnt_exist".to_string());
328 let span = Span::test_data();
329 let mut columns = Record::new();
330 columns.insert(
331 "not_a_column".to_string(),
332 Value::test_string("ThisIsALongString".to_string()),
333 );
334
335 let result = process(&EngineState::new(), table_name, span, &conn, columns);
336
337 assert!(result.is_err());
338 }
339
340 #[test]
341 fn test_errors_when_table_doesnt_exist() {
342 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
343
344 let table_name = Some("test_errors_when_table_doesnt_exist".to_string());
345 let span = Span::test_data();
346 let mut columns = Record::new();
347 columns.insert(
348 "str_column".to_string(),
349 Value::test_string("ThisIsALongString".to_string()),
350 );
351
352 let result = process(&EngineState::new(), table_name, span, &conn, columns);
353
354 assert!(result.is_err());
355 }
356
357 #[test]
358 fn test_insert_json() {
359 let conn = get_shared_mem_conn().expect("Test was unable to get shared connection.");
360
361 let create_stmt = "CREATE TABLE test_insert_json (
362 json_field JSON,
363 jsonb_field JSONB
364 )";
365
366 conn.execute(create_stmt, [])
367 .expect("Failed to create table as part of test.");
368
369 let mut record = Record::new();
370 record.insert("x", Value::test_int(89));
371 record.insert("y", Value::test_int(12));
372 record.insert(
373 "z",
374 Value::test_list(vec![
375 Value::test_string("hello"),
376 Value::test_string("goodbye"),
377 ]),
378 );
379
380 let mut row = Record::new();
381 row.insert("json_field", Value::test_record(record.clone()));
382 row.insert("jsonb_field", Value::test_record(record));
383
384 let result = process(
385 &EngineState::new(),
386 Some("test_insert_json".to_owned()),
387 Span::test_data(),
388 &conn,
389 row,
390 );
391
392 assert!(result.is_ok());
393 }
394}