mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
QQQ-28 implement bulk edit, delete, insert
This commit is contained in:
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
**
|
**
|
||||||
|
Reference in New Issue
Block a user