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) .type(int.class)
.description("Optional limit on the max number of records to include in the export.") .description("Optional limit on the max number of records to include in the export.")
.build()); .build());
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") addCriteriaOption(exportCommand);
.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());
// todo - add the fields as explicit params? // 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) private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table)
{ {
CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create(); CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create();
countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") addCriteriaOption(countCommand);
.type(String[].class)
.build());
// todo - add the fields as explicit params? // todo - add the fields as explicit params?
@ -233,6 +260,7 @@ public class QCommandBuilder
CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create(); CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create();
/* /*
todo - future may accept files, similar to (bulk) insert
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody") updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody")
.type(String.class) .type(String.class)
.build()); .build());
@ -251,10 +279,7 @@ public class QCommandBuilder
*/ */
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey") addPrimaryKeyOrKeysOption(updateCommand, "update");
// type(getClassForField(primaryKeyField))
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.build());
for(QFieldMetaData field : table.getFields().values()) for(QFieldMetaData field : table.getFields().values())
{ {
@ -262,9 +287,14 @@ public class QCommandBuilder
{ {
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName()) updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName())
.type(getClassForField(field)) .type(getClassForField(field))
.description("""
Value to set for the field %s""".formatted(field.getName()))
.build()); .build());
} }
} }
addCriteriaOption(updateCommand);
return 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? .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.build()); .build());
addCriteriaOption(deleteCommand);
return 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.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; 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.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.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
@ -701,6 +702,14 @@ public class QPicoCliImplementation
String json = subParseResult.matchedOptionValue("--mapping", ""); String json = subParseResult.matchedOptionValue("--mapping", "");
mapping = new JsonToQFieldMappingAdapter().buildMappingFromJson(json); 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 // // get the records that the user specified //
@ -782,38 +791,81 @@ public class QPicoCliImplementation
updateInput.setTableName(tableName); updateInput.setTableName(tableName);
QTableMetaData table = qInstance.getTable(tableName); QTableMetaData table = qInstance.getTable(tableName);
List<QRecord> recordList = new ArrayList<>(); List<QRecord> recordsToUpdate = new ArrayList<>();
boolean anyFields = false; boolean anyFields = false;
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); 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(","); Serializable[] primaryKeyValues = primaryKeyOption.split(",");
for(Serializable primaryKeyValue : primaryKeyValues) for(Serializable primaryKeyValue : primaryKeyValues)
{ {
QRecord record = new QRecord(); recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKeyValue));
}
recordList.add(record); }
record.setValue(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()) for(OptionSpec matchedOption : subParseResult.matchedOptions())
{ {
if(matchedOption.longestName().startsWith("--field-")) if(matchedOption.longestName().startsWith("--field-"))
{ {
anyFields = true; 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); String fieldName = matchedOption.longestName().substring(8);
record.setValue(fieldName, matchedOption.getValue()); record.setValue(fieldName, matchedOption.getValue());
} }
} }
} }
if(!anyFields || recordList.isEmpty()) updateInput.setRecords(recordsToUpdate);
{
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
updateInput.setRecords(recordList);
UpdateAction updateAction = new UpdateAction(); UpdateAction updateAction = new UpdateAction();
UpdateOutput updateResult = updateAction.execute(updateInput); UpdateOutput updateResult = updateAction.execute(updateInput);
@ -836,8 +888,23 @@ public class QPicoCliImplementation
// get the pKeys that the user specified // // get the pKeys that the user specified //
///////////////////////////////////////////// /////////////////////////////////////////////
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
Serializable[] primaryKeyValues = primaryKeyOption.split(","); String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues));
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(); DeleteAction deleteAction = new DeleteAction();
DeleteOutput deleteResult = deleteAction.execute(deleteInput); DeleteOutput deleteResult = deleteAction.execute(deleteInput);
@ -865,4 +932,21 @@ public class QPicoCliImplementation
commandLine.usage(commandLine.getOut()); commandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp(); 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 @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); assertRowValueById("person", "first_name", "Garret", 5);
TestOutput testOutput = testCli("person", "update", 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 ** test running a delete against a table
** **
@ -717,7 +791,6 @@ class QPicoCliImplementationTest
/******************************************************************************* /*******************************************************************************
** test exporting a table ** test exporting a table
** **
@ -736,6 +809,8 @@ class QPicoCliImplementationTest
deleteFile(file); deleteFile(file);
} }
/******************************************************************************* /*******************************************************************************
** test exporting a table ** test exporting a table
** **