mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merge remote-tracking branch 'remotes/origin/feature/QQQ-28-bulk-ops-frontend' into feature/sprint-7-integration
This commit is contained in:
2
pom.xml
2
pom.xml
@ -53,7 +53,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.2.0-20220719.154219-3</version>
|
||||
<version>0.2.0-20220725.132738-10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 3rd party deps specifically for this module -->
|
||||
|
@ -28,12 +28,16 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,45 +45,63 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
*******************************************************************************/
|
||||
public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(RDBMSDeleteAction.class);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean supportsQueryFilterInput()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DeleteOutput execute(DeleteInput deleteInput) throws QException
|
||||
{
|
||||
try
|
||||
DeleteOutput deleteOutput = new DeleteOutput();
|
||||
deleteOutput.setRecordsWithErrors(new ArrayList<>());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// Our strategy is: //
|
||||
// - if there's a query filter, try to do a delete WHERE that filter. //
|
||||
// - - if that has an error, or if there wasn't a query filter, then continue: //
|
||||
// - if there's only 1 pkey to delete, just run a delete where $pkey=? query //
|
||||
// - else if there's a list, try to delete it, but upon error: //
|
||||
// - - do a single-delete for each entry in the list. //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
try(Connection connection = getConnection(deleteInput))
|
||||
{
|
||||
DeleteOutput rs = new DeleteOutput();
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
|
||||
String tableName = getTableName(table);
|
||||
String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
|
||||
String sql = "DELETE FROM "
|
||||
+ tableName
|
||||
+ " WHERE "
|
||||
+ primaryKeyName
|
||||
+ " IN ("
|
||||
+ deleteInput.getPrimaryKeys().stream().map(x -> "?").collect(Collectors.joining(","))
|
||||
+ ")";
|
||||
List<Serializable> params = deleteInput.getPrimaryKeys();
|
||||
|
||||
// todo sql customization - can edit sql and/or param list
|
||||
|
||||
try(Connection connection = getConnection(deleteInput))
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a query filter, try to do a single-delete with that filter in the WHERE clause //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(deleteInput.getQueryFilter() != null)
|
||||
{
|
||||
QueryManager.executeUpdateForRowCount(connection, sql, params);
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
rs.setRecords(outputRecords);
|
||||
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
|
||||
try
|
||||
{
|
||||
QRecord qRecord = new QRecord().withTableName(deleteInput.getTableName()).withValue("id", primaryKey);
|
||||
// todo uh, identify any errors?
|
||||
QRecord outputRecord = new QRecord(qRecord);
|
||||
outputRecords.add(outputRecord);
|
||||
deleteInput.getAsyncJobCallback().updateStatus("Running bulk delete via query filter.");
|
||||
deleteQueryFilter(connection, deleteInput, deleteOutput);
|
||||
return (deleteOutput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
deleteInput.getAsyncJobCallback().updateStatus("Error running bulk delete via filter. Fetching keys for individual deletes.");
|
||||
LOG.info("Exception trying to delete by filter query. Moving on to deleting by id now.");
|
||||
deleteInput.setPrimaryKeys(DeleteAction.getPrimaryKeysFromQueryFilter(deleteInput));
|
||||
}
|
||||
}
|
||||
|
||||
return rs;
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// at this point, there either wasn't a query filter, or there was an error executing it (in which case, the query should //
|
||||
// have been converted to a list of primary keys in the deleteInput). so, proceed now by deleting a list of pkeys. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
deleteList(connection, deleteInput, deleteOutput);
|
||||
return (deleteOutput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -87,4 +109,142 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void deleteList(Connection connection, DeleteInput deleteInput, DeleteOutput deleteOutput)
|
||||
{
|
||||
List<Serializable> primaryKeys = deleteInput.getPrimaryKeys();
|
||||
if(primaryKeys.size() == 1)
|
||||
{
|
||||
doDeleteOne(connection, deleteInput.getTable(), primaryKeys.get(0), deleteOutput);
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo - page this? or binary-tree it?
|
||||
try
|
||||
{
|
||||
deleteInput.getAsyncJobCallback().updateStatus("Running bulk delete via key list.");
|
||||
doDeleteList(connection, deleteInput.getTable(), primaryKeys, deleteOutput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
deleteInput.getAsyncJobCallback().updateStatus("Error running bulk delete via key list. Performing individual deletes.");
|
||||
LOG.info("Caught an error doing list-delete - going to single-deletes now", e);
|
||||
int current = 1;
|
||||
for(Serializable primaryKey : primaryKeys)
|
||||
{
|
||||
deleteInput.getAsyncJobCallback().updateStatus(current++, primaryKeys.size());
|
||||
doDeleteOne(connection, deleteInput.getTable(), primaryKey, deleteOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void doDeleteOne(Connection connection, QTableMetaData table, Serializable primaryKey, DeleteOutput deleteOutput)
|
||||
{
|
||||
String tableName = getTableName(table);
|
||||
String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
|
||||
|
||||
// todo sql customization - can edit sql and/or param list?
|
||||
String sql = "DELETE FROM "
|
||||
+ tableName
|
||||
+ " WHERE "
|
||||
+ primaryKeyName + " = ?";
|
||||
|
||||
try
|
||||
{
|
||||
int rowCount = QueryManager.executeUpdateForRowCount(connection, sql, primaryKey);
|
||||
deleteOutput.addToDeletedRecordCount(rowCount);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// it seems like maybe we shouldn't do the below - ids that aren't found will hit this condition, //
|
||||
// but we (1) don't care and (2) can't detect this case when doing an in-list delete, so, let's //
|
||||
// make the results match, and just avoid adding to the deleted count, not marking it as an error. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if(rowCount == 1)
|
||||
// {
|
||||
// deleteOutput.addToDeletedRecordCount(1);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOG.debug("rowCount 0 trying to delete [" + tableName + "][" + primaryKey + "]");
|
||||
// deleteOutput.addRecordWithError(new QRecord(table, primaryKey).withError("Record was not deleted (but no error was given from the database)"));
|
||||
// }
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Exception trying to delete [" + tableName + "][" + primaryKey + "]", e);
|
||||
deleteOutput.addRecordWithError(new QRecord(table, primaryKey).withError("Record was not deleted: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void doDeleteList(Connection connection, QTableMetaData table, List<Serializable> primaryKeys, DeleteOutput deleteOutput) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
String tableName = getTableName(table);
|
||||
String primaryKeyName = getColumnName(table.getField(table.getPrimaryKeyField()));
|
||||
String sql = "DELETE FROM "
|
||||
+ tableName
|
||||
+ " WHERE "
|
||||
+ primaryKeyName
|
||||
+ " IN ("
|
||||
+ primaryKeys.stream().map(x -> "?").collect(Collectors.joining(","))
|
||||
+ ")";
|
||||
|
||||
// todo sql customization - can edit sql and/or param list
|
||||
|
||||
Integer rowCount = QueryManager.executeUpdateForRowCount(connection, sql, primaryKeys);
|
||||
deleteOutput.addToDeletedRecordCount(rowCount);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new QException("Error executing delete: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void deleteQueryFilter(Connection connection, DeleteInput deleteInput, DeleteOutput deleteOutput) throws QException
|
||||
{
|
||||
QQueryFilter filter = deleteInput.getQueryFilter();
|
||||
List<Serializable> params = new ArrayList<>();
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
|
||||
String tableName = getTableName(table);
|
||||
String whereClause = makeWhereClause(table, filter.getCriteria(), params);
|
||||
|
||||
// todo sql customization - can edit sql and/or param list?
|
||||
String sql = "DELETE FROM "
|
||||
+ tableName
|
||||
+ " WHERE "
|
||||
+ whereClause;
|
||||
|
||||
try
|
||||
{
|
||||
int rowCount = QueryManager.executeUpdateForRowCount(connection, sql, params);
|
||||
|
||||
deleteOutput.setDeletedRecordCount(rowCount);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new QException("Error executing delete with filter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,15 +89,18 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
|
||||
.map(x -> "?")
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
String tableName = getTableName(table);
|
||||
StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES");
|
||||
List<Object> params = new ArrayList<>();
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
rs.setRecords(outputRecords);
|
||||
|
||||
try(Connection connection = getConnection(insertInput))
|
||||
{
|
||||
for(List<QRecord> page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE))
|
||||
{
|
||||
int recordIndex = 0;
|
||||
String tableName = getTableName(table);
|
||||
StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES");
|
||||
List<Object> params = new ArrayList<>();
|
||||
int recordIndex = 0;
|
||||
|
||||
for(QRecord record : page)
|
||||
{
|
||||
if(recordIndex++ > 0)
|
||||
@ -116,11 +119,9 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
|
||||
// todo sql customization - can edit sql and/or param list
|
||||
// todo - non-serial-id style tables
|
||||
// todo - other generated values, e.g., createDate... maybe need to re-select?
|
||||
List<Integer> idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params);
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
rs.setRecords(outputRecords);
|
||||
int index = 0;
|
||||
for(QRecord record : insertInput.getRecords())
|
||||
List<Integer> idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params);
|
||||
int index = 0;
|
||||
for(QRecord record : page)
|
||||
{
|
||||
Integer id = idList.get(index++);
|
||||
QRecord outputRecord = new QRecord(record);
|
||||
|
@ -58,6 +58,8 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(RDBMSUpdateAction.class);
|
||||
|
||||
private int statusCounter = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -119,7 +121,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(List<String> fieldsBeingUpdated : recordsByFieldBeingUpdated.keySet())
|
||||
{
|
||||
updateRecordsWithMatchingListOfFields(connection, table, recordsByFieldBeingUpdated.get(fieldsBeingUpdated), fieldsBeingUpdated);
|
||||
updateRecordsWithMatchingListOfFields(updateInput, connection, table, recordsByFieldBeingUpdated.get(fieldsBeingUpdated), fieldsBeingUpdated);
|
||||
}
|
||||
|
||||
return rs;
|
||||
@ -136,15 +138,25 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void updateRecordsWithMatchingListOfFields(Connection connection, QTableMetaData table, List<QRecord> recordList, List<String> fieldsBeingUpdated) throws SQLException
|
||||
private void updateRecordsWithMatchingListOfFields(UpdateInput updateInput, Connection connection, QTableMetaData table, List<QRecord> recordList, List<String> fieldsBeingUpdated) throws SQLException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// check for an optimization - if all of the records have the same values for //
|
||||
// all fields being updated, just do 1 update, with an IN list on the ids. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(areAllValuesBeingUpdatedTheSame(recordList, fieldsBeingUpdated))
|
||||
boolean allAreTheSame;
|
||||
if(updateInput.getAreAllValuesBeingUpdatedTheSame() != null)
|
||||
{
|
||||
updateRecordsWithMatchingValuesAndFields(connection, table, recordList, fieldsBeingUpdated);
|
||||
allAreTheSame = updateInput.getAreAllValuesBeingUpdatedTheSame();
|
||||
}
|
||||
else
|
||||
{
|
||||
allAreTheSame = areAllValuesBeingUpdatedTheSame(recordList, fieldsBeingUpdated);
|
||||
}
|
||||
|
||||
if(allAreTheSame)
|
||||
{
|
||||
updateRecordsWithMatchingValuesAndFields(updateInput, connection, table, recordList, fieldsBeingUpdated);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,10 +186,12 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
// let query manager do the batch updates - note that it will internally page //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
QueryManager.executeBatchUpdate(connection, sql, values);
|
||||
incrementStatus(updateInput, recordList.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -198,7 +212,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void updateRecordsWithMatchingValuesAndFields(Connection connection, QTableMetaData table, List<QRecord> recordList, List<String> fieldsBeingUpdated) throws SQLException
|
||||
private void updateRecordsWithMatchingValuesAndFields(UpdateInput updateInput, Connection connection, QTableMetaData table, List<QRecord> recordList, List<String> fieldsBeingUpdated) throws SQLException
|
||||
{
|
||||
for(List<QRecord> page : CollectionUtils.getPages(recordList, QueryManager.PAGE_SIZE))
|
||||
{
|
||||
@ -230,6 +244,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
// let query manager do the update //
|
||||
/////////////////////////////////////
|
||||
QueryManager.executeUpdate(connection, sql, params);
|
||||
incrementStatus(updateInput, page.size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,4 +276,14 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte
|
||||
return (true);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void incrementStatus(UpdateInput updateInput, int amount)
|
||||
{
|
||||
statusCounter += amount;
|
||||
updateInput.getAsyncJobCallback().updateStatus(statusCounter, updateInput.getRecords().size());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -55,7 +55,9 @@ import org.apache.commons.lang.NotImplementedException;
|
||||
*******************************************************************************/
|
||||
public class QueryManager
|
||||
{
|
||||
public static final int PAGE_SIZE = 2000;
|
||||
public static final int DEFAULT_PAGE_SIZE = 2000;
|
||||
public static int PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
||||
|
||||
private static final int MS_PER_SEC = 1000;
|
||||
private static final int NINETEEN_HUNDRED = 1900;
|
||||
|
||||
@ -93,8 +95,8 @@ public class QueryManager
|
||||
try
|
||||
{
|
||||
statement = prepareStatementAndBindParams(connection, sql, params);
|
||||
statement.execute();
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
statement.execute();
|
||||
resultSet = statement.getResultSet();
|
||||
|
||||
procesor.processResultSet(resultSet);
|
||||
@ -354,8 +356,8 @@ public class QueryManager
|
||||
public static PreparedStatement executeUpdate(Connection connection, String sql, Object... params) throws SQLException
|
||||
{
|
||||
PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params);
|
||||
statement.executeUpdate();
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
statement.executeUpdate();
|
||||
return (statement);
|
||||
}
|
||||
|
||||
@ -367,8 +369,8 @@ public class QueryManager
|
||||
public static PreparedStatement executeUpdate(Connection connection, String sql, List<Object> params) throws SQLException
|
||||
{
|
||||
PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params);
|
||||
statement.executeUpdate();
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
statement.executeUpdate();
|
||||
return (statement);
|
||||
}
|
||||
|
||||
@ -413,8 +415,8 @@ public class QueryManager
|
||||
{
|
||||
try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params))
|
||||
{
|
||||
statement.executeUpdate();
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
statement.executeUpdate();
|
||||
return (statement.getUpdateCount());
|
||||
}
|
||||
}
|
||||
@ -473,9 +475,9 @@ public class QueryManager
|
||||
try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS))
|
||||
{
|
||||
bindParams(params.toArray(), statement);
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
statement.executeUpdate();
|
||||
ResultSet generatedKeys = statement.getGeneratedKeys();
|
||||
incrementStatistic(STAT_QUERIES_RAN);
|
||||
while(generatedKeys.next())
|
||||
{
|
||||
rs.add(getInteger(generatedKeys, 1));
|
||||
@ -565,8 +567,8 @@ public class QueryManager
|
||||
bindParams(updatePS, params);
|
||||
updatePS.addBatch();
|
||||
}
|
||||
updatePS.executeBatch();
|
||||
incrementStatistic(STAT_BATCHES_RAN);
|
||||
updatePS.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1617,4 +1619,25 @@ public class QueryManager
|
||||
return statistics;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Note - this changes a static field that impacts all usages. Really, it's meant
|
||||
** to only be called in unit tests (at least as of the time of this writing).
|
||||
*******************************************************************************/
|
||||
public static void setPageSize(int pageSize)
|
||||
{
|
||||
PAGE_SIZE = pageSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void resetPageSize()
|
||||
{
|
||||
PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,6 +36,10 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDe
|
||||
public class TestUtils
|
||||
{
|
||||
|
||||
public static final String DEFAULT_BACKEND_NAME = "default";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -54,13 +58,12 @@ public class TestUtils
|
||||
*******************************************************************************/
|
||||
public static RDBMSBackendMetaData defineBackend()
|
||||
{
|
||||
RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData()
|
||||
return (new RDBMSBackendMetaData()
|
||||
.withName(DEFAULT_BACKEND_NAME)
|
||||
.withVendor("h2")
|
||||
.withHostName("mem")
|
||||
.withDatabaseName("test_database")
|
||||
.withUsername("sa");
|
||||
rdbmsBackendMetaData.setName("default");
|
||||
return (rdbmsBackendMetaData);
|
||||
.withUsername("sa"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
|
||||
|
||||
@ -38,17 +39,39 @@ import static junit.framework.Assert.assertNotNull;
|
||||
public class RDBMSActionTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
private void afterEachRDBMSActionTest()
|
||||
{
|
||||
QueryManager.resetPageSize();
|
||||
QueryManager.resetStatistics();
|
||||
QueryManager.setCollectStatistics(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected void primeTestDatabase() throws Exception
|
||||
{
|
||||
primeTestDatabase("prime-test-database.sql");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void primeTestDatabase() throws Exception
|
||||
protected void primeTestDatabase(String sqlFileName) throws Exception
|
||||
{
|
||||
ConnectionManager connectionManager = new ConnectionManager();
|
||||
try(Connection connection = connectionManager.getConnection(TestUtils.defineBackend()))
|
||||
{
|
||||
InputStream primeTestDatabaseSqlStream = RDBMSActionTest.class.getResourceAsStream("/prime-test-database.sql");
|
||||
InputStream primeTestDatabaseSqlStream = RDBMSActionTest.class.getResourceAsStream("/" + sqlFileName);
|
||||
assertNotNull(primeTestDatabaseSqlStream);
|
||||
List<String> lines = (List<String>) IOUtils.readLines(primeTestDatabaseSqlStream);
|
||||
lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
|
||||
|
@ -23,9 +23,16 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -56,11 +63,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest
|
||||
@Test
|
||||
public void testDeleteAll() throws Exception
|
||||
{
|
||||
DeleteInput deleteInput = initDeleteRequest();
|
||||
DeleteInput deleteInput = initStandardPersonDeleteRequest();
|
||||
deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5));
|
||||
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
|
||||
assertEquals(5, deleteResult.getRecords().size(), "Unfiltered delete should return all rows");
|
||||
// todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
|
||||
assertEquals(5, deleteResult.getDeletedRecordCount(), "Unfiltered delete should return all rows");
|
||||
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
|
||||
runTestSql("SELECT id FROM person", (rs -> assertFalse(rs.next())));
|
||||
}
|
||||
|
||||
@ -72,11 +79,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest
|
||||
@Test
|
||||
public void testDeleteOne() throws Exception
|
||||
{
|
||||
DeleteInput deleteInput = initDeleteRequest();
|
||||
DeleteInput deleteInput = initStandardPersonDeleteRequest();
|
||||
deleteInput.setPrimaryKeys(List.of(1));
|
||||
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
|
||||
assertEquals(1, deleteResult.getRecords().size(), "Should delete one row");
|
||||
// todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
|
||||
assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row");
|
||||
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
|
||||
runTestSql("SELECT id FROM person WHERE id = 1", (rs -> assertFalse(rs.next())));
|
||||
}
|
||||
|
||||
@ -88,11 +95,11 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest
|
||||
@Test
|
||||
public void testDeleteSome() throws Exception
|
||||
{
|
||||
DeleteInput deleteInput = initDeleteRequest();
|
||||
DeleteInput deleteInput = initStandardPersonDeleteRequest();
|
||||
deleteInput.setPrimaryKeys(List.of(1, 3, 5));
|
||||
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
|
||||
assertEquals(3, deleteResult.getRecords().size(), "Should delete one row");
|
||||
// todo - add errors to QRecord? assertTrue(deleteResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
|
||||
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should delete one row");
|
||||
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
|
||||
runTestSql("SELECT id FROM person", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
@ -110,7 +117,22 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private DeleteInput initDeleteRequest()
|
||||
@Test
|
||||
void testDeleteSomeIdsThatExistAndSomeThatDoNot() throws Exception
|
||||
{
|
||||
DeleteInput deleteInput = initStandardPersonDeleteRequest();
|
||||
deleteInput.setPrimaryKeys(List.of(1, -1));
|
||||
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
|
||||
assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row");
|
||||
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors (the one not found is just noop)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private DeleteInput initStandardPersonDeleteRequest()
|
||||
{
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setInstance(TestUtils.defineInstance());
|
||||
@ -118,4 +140,87 @@ public class RDBMSDeleteActionTest extends RDBMSActionTest
|
||||
return deleteInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void testDeleteWhereForeignKeyBlocksSome() throws Exception
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// load the parent-child tables, with foreign keys and instance //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
super.primeTestDatabase("prime-test-database-parent-child-tables.sql");
|
||||
DeleteInput deleteInput = initChildTableInstanceAndDeleteRequest();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// try to delete all of the child records - 2 should fail, because they are referenced by parent_table.child_id //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5));
|
||||
|
||||
QueryManager.setCollectStatistics(true);
|
||||
QueryManager.resetStatistics();
|
||||
|
||||
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// assert that 6 queries ran - the initial delete (which failed), then 6 more //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
QueryManager.setCollectStatistics(false);
|
||||
Map<String, Integer> queryStats = QueryManager.getStatistics();
|
||||
assertEquals(6, queryStats.get(QueryManager.STAT_QUERIES_RAN), "Number of queries ran");
|
||||
|
||||
assertEquals(2, deleteResult.getRecordsWithErrors().size(), "Should get back the 2 records with errors");
|
||||
assertTrue(deleteResult.getRecordsWithErrors().stream().noneMatch(r -> r.getErrors().isEmpty()), "All we got back should have errors");
|
||||
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should get back that 3 were deleted");
|
||||
|
||||
runTestSql("SELECT id FROM child_table", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
{
|
||||
rowsFound++;
|
||||
///////////////////////////////////////////
|
||||
// child_table rows 1 & 3 should survive //
|
||||
///////////////////////////////////////////
|
||||
assertTrue(rs.getInt(1) == 1 || rs.getInt(1) == 3);
|
||||
}
|
||||
assertEquals(2, rowsFound);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private DeleteInput initChildTableInstanceAndDeleteRequest()
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
|
||||
String childTableName = "childTable";
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(childTableName)
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withBackendDetails(new RDBMSTableBackendDetails()
|
||||
.withTableName("child_table")));
|
||||
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName("parentTable")
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("childId", QFieldType.INTEGER).withBackendName("child_id"))
|
||||
.withBackendDetails(new RDBMSTableBackendDetails()
|
||||
.withTableName("parent_table")));
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setInstance(qInstance);
|
||||
deleteInput.setTableName(childTableName);
|
||||
return deleteInput;
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -97,17 +98,7 @@ public class RDBMSInsertActionTest extends RDBMSActionTest
|
||||
assertEquals(1, insertOutput.getRecords().size(), "Should return 1 row");
|
||||
assertNotNull(insertOutput.getRecords().get(0).getValue("id"), "Should have an id in the row");
|
||||
// todo - add errors to QRecord? assertTrue(insertResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
|
||||
runTestSql("SELECT * FROM person WHERE last_name = 'Kirk'", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
{
|
||||
rowsFound++;
|
||||
assertEquals(6, rs.getInt("id"));
|
||||
assertEquals("James", rs.getString("first_name"));
|
||||
assertNotNull(rs.getString("create_date"));
|
||||
}
|
||||
assertEquals(1, rowsFound);
|
||||
}));
|
||||
assertAnInsertedPersonRecord("James", "Kirk", 6);
|
||||
}
|
||||
|
||||
|
||||
@ -118,6 +109,8 @@ public class RDBMSInsertActionTest extends RDBMSActionTest
|
||||
@Test
|
||||
public void testInsertMany() throws Exception
|
||||
{
|
||||
QueryManager.setPageSize(2);
|
||||
|
||||
InsertInput insertInput = initInsertRequest();
|
||||
QRecord record1 = new QRecord().withTableName("person")
|
||||
.withValue("firstName", "Jean-Luc")
|
||||
@ -129,29 +122,35 @@ public class RDBMSInsertActionTest extends RDBMSActionTest
|
||||
.withValue("lastName", "Riker")
|
||||
.withValue("email", "notthomas@starfleet.net")
|
||||
.withValue("birthDate", "2320-05-20");
|
||||
insertInput.setRecords(List.of(record1, record2));
|
||||
QRecord record3 = new QRecord().withTableName("person")
|
||||
.withValue("firstName", "Beverly")
|
||||
.withValue("lastName", "Crusher")
|
||||
.withValue("email", "doctor@starfleet.net")
|
||||
.withValue("birthDate", "2320-06-26");
|
||||
insertInput.setRecords(List.of(record1, record2, record3));
|
||||
InsertOutput insertOutput = new RDBMSInsertAction().execute(insertInput);
|
||||
assertEquals(2, insertOutput.getRecords().size(), "Should return 1 row");
|
||||
assertEquals(3, insertOutput.getRecords().size(), "Should return right # of rows");
|
||||
assertEquals(6, insertOutput.getRecords().get(0).getValue("id"), "Should have next id in the row");
|
||||
assertEquals(7, insertOutput.getRecords().get(1).getValue("id"), "Should have next id in the row");
|
||||
// todo - add errors to QRecord? assertTrue(insertResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
|
||||
runTestSql("SELECT * FROM person WHERE last_name = 'Picard'", (rs -> {
|
||||
assertEquals(8, insertOutput.getRecords().get(2).getValue("id"), "Should have next id in the row");
|
||||
assertAnInsertedPersonRecord("Jean-Luc", "Picard", 6);
|
||||
assertAnInsertedPersonRecord("William", "Riker", 7);
|
||||
assertAnInsertedPersonRecord("Beverly", "Crusher", 8);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void assertAnInsertedPersonRecord(String firstName, String lastName, Integer id) throws Exception
|
||||
{
|
||||
runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
{
|
||||
rowsFound++;
|
||||
assertEquals(6, rs.getInt("id"));
|
||||
assertEquals("Jean-Luc", rs.getString("first_name"));
|
||||
}
|
||||
assertEquals(1, rowsFound);
|
||||
}));
|
||||
runTestSql("SELECT * FROM person WHERE last_name = 'Riker'", (rs -> {
|
||||
int rowsFound = 0;
|
||||
while(rs.next())
|
||||
{
|
||||
rowsFound++;
|
||||
assertEquals(7, rs.getInt("id"));
|
||||
assertEquals("William", rs.getString("first_name"));
|
||||
assertEquals(id, rs.getInt("id"));
|
||||
assertEquals(firstName, rs.getString("first_name"));
|
||||
assertNotNull(rs.getString("create_date"));
|
||||
assertNotNull(rs.getString("modify_date"));
|
||||
}
|
||||
assertEquals(1, rowsFound);
|
||||
}));
|
||||
|
@ -30,13 +30,15 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
@ -59,17 +61,6 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
public void afterEach() throws Exception
|
||||
{
|
||||
QueryManager.resetStatistics();
|
||||
QueryManager.setCollectStatistics(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -311,6 +302,45 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testModifyDateGetsUpdated() throws Exception
|
||||
{
|
||||
String originalModifyDate = selectModifyDate(1);
|
||||
|
||||
UpdateInput updateInput = initUpdateRequest();
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
records.add(new QRecord().withTableName("person")
|
||||
.withValue("id", 1)
|
||||
.withValue("firstName", "Johnny Updated"));
|
||||
updateInput.setRecords(records);
|
||||
new RDBMSUpdateAction().execute(updateInput);
|
||||
|
||||
String updatedModifyDate = selectModifyDate(1);
|
||||
|
||||
assertTrue(StringUtils.hasContent(originalModifyDate));
|
||||
assertTrue(StringUtils.hasContent(updatedModifyDate));
|
||||
assertNotEquals(originalModifyDate, updatedModifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String selectModifyDate(Integer id) throws Exception
|
||||
{
|
||||
StringBuilder modifyDate = new StringBuilder();
|
||||
runTestSql("SELECT modify_date FROM person WHERE id = " + id, (rs -> {
|
||||
if(rs.next())
|
||||
{
|
||||
modifyDate.append(rs.getString("modify_date"));
|
||||
}
|
||||
}));
|
||||
return (modifyDate.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,48 @@
|
||||
--
|
||||
-- QQQ - Low-code Application Framework for Engineers.
|
||||
-- Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
-- contact@kingsrook.com
|
||||
-- https://github.com/Kingsrook/
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as
|
||||
-- published by the Free Software Foundation, either version 3 of the
|
||||
-- License, or (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS child_table;
|
||||
CREATE TABLE child_table
|
||||
(
|
||||
id INT AUTO_INCREMENT primary key,
|
||||
name VARCHAR(80) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO child_table (id, name) VALUES (1, 'Timmy');
|
||||
INSERT INTO child_table (id, name) VALUES (2, 'Jimmy');
|
||||
INSERT INTO child_table (id, name) VALUES (3, 'Johnny');
|
||||
INSERT INTO child_table (id, name) VALUES (4, 'Gracie');
|
||||
INSERT INTO child_table (id, name) VALUES (5, 'Suzie');
|
||||
|
||||
DROP TABLE IF EXISTS parent_table;
|
||||
CREATE TABLE parent_table
|
||||
(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(80) NOT NULL,
|
||||
child_id INT,
|
||||
foreign key (child_id) references child_table(id)
|
||||
);
|
||||
|
||||
INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1);
|
||||
INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1);
|
||||
INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null);
|
||||
INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null);
|
||||
INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3);
|
Reference in New Issue
Block a user