QQQ-28 implement bulk edit, delete, insert

This commit is contained in:
2022-07-27 09:15:47 -05:00
parent 984a650d8c
commit f17514c608
3 changed files with 230 additions and 39 deletions

View File

@ -179,10 +179,7 @@ public class QCommandBuilder
.type(int.class)
.description("Optional limit on the max number of records to include in the export.")
.build());
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
.type(String[].class)
.description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42")
.build());
addCriteriaOption(exportCommand);
// todo - add the fields as explicit params?
@ -191,6 +188,38 @@ public class QCommandBuilder
/*******************************************************************************
** add the standard '--criteria' option
*******************************************************************************/
private void addCriteriaOption(CommandLine.Model.CommandSpec commandSpec)
{
commandSpec.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
.type(String[].class)
.description("""
Query filter criteria. May be given multiple times.
Use format: "$fieldName $operator $value".
e.g., "id EQUALS 42\"""")
.build());
}
/*******************************************************************************
**
*******************************************************************************/
private void addPrimaryKeyOrKeysOption(CommandLine.Model.CommandSpec updateCommand, String verbForDescription)
{
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
// type(getClassForField(primaryKeyField))
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.description("""
Primary Key(s) for the records to %s.
May provide multiple values, separated by commas""".formatted(verbForDescription))
.build());
}
/*******************************************************************************
**
*******************************************************************************/
@ -214,9 +243,7 @@ public class QCommandBuilder
private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table)
{
CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create();
countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
.type(String[].class)
.build());
addCriteriaOption(countCommand);
// todo - add the fields as explicit params?
@ -233,6 +260,7 @@ public class QCommandBuilder
CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create();
/*
todo - future may accept files, similar to (bulk) insert
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody")
.type(String.class)
.build());
@ -251,10 +279,7 @@ public class QCommandBuilder
*/
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
// type(getClassForField(primaryKeyField))
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.build());
addPrimaryKeyOrKeysOption(updateCommand, "update");
for(QFieldMetaData field : table.getFields().values())
{
@ -262,9 +287,14 @@ public class QCommandBuilder
{
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName())
.type(getClassForField(field))
.description("""
Value to set for the field %s""".formatted(field.getName()))
.build());
}
}
addCriteriaOption(updateCommand);
return updateCommand;
}
@ -315,6 +345,8 @@ public class QCommandBuilder
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.build());
addCriteriaOption(deleteCommand);
return deleteCommand;
}

View File

@ -62,6 +62,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
@ -701,6 +702,14 @@ public class QPicoCliImplementation
String json = subParseResult.matchedOptionValue("--mapping", "");
mapping = new JsonToQFieldMappingAdapter().buildMappingFromJson(json);
}
else
{
mapping = new QKeyBasedFieldMapping();
for(Map.Entry<String, QFieldMetaData> entry : table.getFields().entrySet())
{
((QKeyBasedFieldMapping) mapping).addMapping(entry.getKey(), entry.getValue().getLabel());
}
}
/////////////////////////////////////////////
// get the records that the user specified //
@ -782,38 +791,81 @@ public class QPicoCliImplementation
updateInput.setTableName(tableName);
QTableMetaData table = qInstance.getTable(tableName);
List<QRecord> recordList = new ArrayList<>();
List<QRecord> recordsToUpdate = new ArrayList<>();
boolean anyFields = false;
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
if(StringUtils.hasContent(primaryKeyOption))
{
//////////////////////////////////////////////////////////////////////////////////////
// if the primaryKey option was given, split it up and seed the recordToUpdate list //
//////////////////////////////////////////////////////////////////////////////////////
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
for(Serializable primaryKeyValue : primaryKeyValues)
{
QRecord record = new QRecord();
recordList.add(record);
record.setValue(table.getPrimaryKeyField(), primaryKeyValue);
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKeyValue));
}
}
else if(criteria.length > 0)
{
//////////////////////////////////////////////////////////////////////////////////////
// else if criteria were given, execute the query for the lsit of records to update //
//////////////////////////////////////////////////////////////////////////////////////
for(QRecord qRecord : executeQuery(tableName, subParseResult))
{
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), qRecord.getValue(table.getPrimaryKeyField())));
}
}
else
{
commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
///////////////////////////////////////////////////
// make sure at least one --field- arg was given //
///////////////////////////////////////////////////
for(OptionSpec matchedOption : subParseResult.matchedOptions())
{
if(matchedOption.longestName().startsWith("--field-"))
{
anyFields = true;
}
}
if(!anyFields)
{
commandLine.getErr().println("Error: At least one field to update must be specified.");
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
if(recordsToUpdate.isEmpty())
{
commandLine.getErr().println("No rows to update were found.");
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
for(QRecord record : recordsToUpdate)
{
for(OptionSpec matchedOption : subParseResult.matchedOptions())
{
if(matchedOption.longestName().startsWith("--field-"))
{
String fieldName = matchedOption.longestName().substring(8);
record.setValue(fieldName, matchedOption.getValue());
}
}
}
if(!anyFields || recordList.isEmpty())
{
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
updateInput.setRecords(recordList);
updateInput.setRecords(recordsToUpdate);
UpdateAction updateAction = new UpdateAction();
UpdateOutput updateResult = updateAction.execute(updateInput);
@ -836,8 +888,23 @@ public class QPicoCliImplementation
// get the pKeys that the user specified //
/////////////////////////////////////////////
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues));
String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
if(StringUtils.hasContent(primaryKeyOption))
{
deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyOption.split(",")));
}
else if(criteria.length > 0)
{
deleteInput.setQueryFilter(generateQueryFilter(subParseResult));
}
else
{
commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
CommandLine subCommandLine = commandLine.getSubcommands().get("delete");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
DeleteAction deleteAction = new DeleteAction();
DeleteOutput deleteResult = deleteAction.execute(deleteInput);
@ -865,4 +932,21 @@ public class QPicoCliImplementation
commandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> executeQuery(String tableName, ParseResult subParseResult) throws QException
{
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(tableName);
queryInput.setFilter(generateQueryFilter(subParseResult));
QueryAction queryAction = new QueryAction();
QueryOutput queryOutput = queryAction.execute(queryInput);
return (queryOutput.getRecords());
}
}

View File

@ -447,11 +447,24 @@ class QPicoCliImplementationTest
/*******************************************************************************
** test running an update w/ fields as arguments
** test running an update w/o specifying any pkeys or criteria, prints usage
**
*******************************************************************************/
@Test
public void test_tableUpdateFieldArguments() throws Exception
public void test_tableUpdateNoRecordsPrintsUsage()
{
TestOutput testOutput = testCli("person", "update", "--field-firstName=Lucy");
assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person update");
}
/*******************************************************************************
** test running an update w/ fields as arguments and one primary key
**
*******************************************************************************/
@Test
public void test_tableUpdateFieldArgumentsOnePrimaryKey() throws Exception
{
assertRowValueById("person", "first_name", "Garret", 5);
TestOutput testOutput = testCli("person", "update",
@ -467,6 +480,55 @@ class QPicoCliImplementationTest
/*******************************************************************************
** test running an update w/ fields as arguments and multiple primary keys
**
*******************************************************************************/
@Test
public void test_tableUpdateFieldArgumentsManyPrimaryKeys() throws Exception
{
assertRowValueById("person", "first_name", "Tyler", 4);
assertRowValueById("person", "first_name", "Garret", 5);
TestOutput testOutput = testCli("person", "update",
"--primaryKey=4,5",
"--field-firstName=Lucy",
"--field-lastName=Lu");
JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(updateResult);
assertEquals(2, updateResult.getJSONArray("records").length());
assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
assertRowValueById("person", "first_name", "Lucy", 4);
assertRowValueById("person", "first_name", "Lucy", 5);
}
/*******************************************************************************
** test running an update w/ fields as arguments and a criteria
**
*******************************************************************************/
@Test
public void test_tableUpdateFieldArgumentsCriteria() throws Exception
{
assertRowValueById("person", "first_name", "Tyler", 4);
assertRowValueById("person", "first_name", "Garret", 5);
TestOutput testOutput = testCli("person", "update",
"--criteria",
"id GREATER_THAN_OR_EQUALS 4",
"--field-firstName=Lucy",
"--field-lastName=Lu");
JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(updateResult);
assertEquals(2, updateResult.getJSONArray("records").length());
assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
assertRowValueById("person", "first_name", "Lucy", 4);
assertRowValueById("person", "first_name", "Lucy", 5);
}
/*******************************************************************************
**
*******************************************************************************/
@ -486,6 +548,18 @@ class QPicoCliImplementationTest
/*******************************************************************************
** test running a delete without enough args
**
*******************************************************************************/
@Test
public void test_tableDeleteWithoutArgs() throws Exception
{
TestOutput testOutput = testCli("person", "delete");
assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person delete");
}
/*******************************************************************************
** test running a delete against a table
**
@ -717,7 +791,6 @@ class QPicoCliImplementationTest
/*******************************************************************************
** test exporting a table
**
@ -736,6 +809,8 @@ class QPicoCliImplementationTest
deleteFile(file);
}
/*******************************************************************************
** test exporting a table
**