提交 176dc52c authored 作者: Owner's avatar Owner

Added match-select query for ROWID checking

上级 73baaefd
......@@ -1192,6 +1192,33 @@ public class Parser {
}
setSQL(command, "MERGE", start);
// build and prepare the targetMatchQuery ready to test each rows existence in the target table (using source row to match)
StringBuffer targetMatchQuerySQL = new StringBuffer("SELECT _ROWID_ FROM "+command.getTargetTable().getName());
if(command.getTargetTableFilter().getTableAlias()!=null){
targetMatchQuerySQL.append(" AS "+command.getTargetTableFilter().getTableAlias());
}
targetMatchQuerySQL.append(" WHERE "+command.getOnCondition().getSQL());
// Select preparedTargetMatchQuery = new Select(session);
// preparedTargetMatchQuery.addTableFilter(command.getTargetTableFilter(), true/*isTop*/);
// preparedTargetMatchQuery.addTableFilter(command.getSourceTableFilter(), false/*isTop - not top table scan*/);
// preparedTargetMatchQuery.setSQL(targetMatchQuerySQL.toString());
// ArrayList<Expression> selectList = New.arrayList();
// //Database db = session == null ? null : session.getDatabase();
// selectList.add(new ExpressionColumn(session.getDatabase(), command.getTargetTableFilter().getTable().getRowIdColumn()));
// preparedTargetMatchQuery.setExpressions(selectList);
// preparedTargetMatchQuery.init();
command.setTargetMatchQuery((Select)parse(targetMatchQuerySQL.toString()));
// Select command = new Select(session);
// currentSelect = command;
// TableFilter filter = parseValuesTable(0);
// ArrayList<Expression> list = New.arrayList();
// list.add(new Wildcard(null, null));
// command.setExpressions(list);
// command.addTableFilter(filter, true);
// command.init();
return command;
}
......
......@@ -78,22 +78,21 @@ import org.h2.value.Value;
* The ON clause conditions are always prepended to the WHERE clause of these embedded
* statements, so they will never update more than the ON join condition.
*
* POTENTIAL ISSUES
*
* 1) If neither UPDATE or DELETE clause is supplied, but INSERT is supplied - the INSERT
* 4) Previously if neither UPDATE or DELETE clause is supplied, but INSERT is supplied - the INSERT
* action is always triggered. This is because the embedded UPDATE and DELETE statement's
* returned update row count is used to detect a matching join.
* If neither of the two the statements are provided, no matching join is EVER detected.
* A fix for this is to generate a "matchSelect" query and use that to always detect
* A fix for this is now implemented as described below:
* We now generate a "matchSelect" query and use that to always detect
* a match join - rather than relying on UPDATE or DELETE statements.
*
* This would be an improvement, especially in the case that if either of the
* This is an improvement, especially in the case that if either of the
* UPDATE or DELETE statements had their own fine-grained WHERE conditions, making
* them completely different conditions than the plain ON condition clause which
* the SQL author would be specifying/expecting.
*
* An additional benefit of this solution would also be that the this "matchSelect" query
* could be used to return the ROWID of the found (or inserted) query - for more accurate
* An additional benefit of this solution is that this "matchSelect" query
* is used to return the ROWID of the found (or inserted) query - for more accurate
* enforcing of the only-update-each-target-row-once rule.
*/
public class MergeUsing extends Prepared {
......@@ -116,6 +115,9 @@ public class MergeUsing extends Prepared {
private int countUpdatedRows=0;
private Column[] sourceKeys;
private HashMap<List<Value>,Integer> sourceKeysRemembered = new HashMap<List<Value>,Integer>();
private Select targetMatchQuery;
private HashMap<Value, Integer> targetRowidsRemembered = new HashMap<Value,Integer>();
private int sourceQueryRowNumber= 0;
public MergeUsing(Merge merge) {
super(merge.getSession());
......@@ -129,8 +131,9 @@ public class MergeUsing extends Prepared {
@Override
public int update() {
// clear list of source table keys we have processed already
// clear list of source table keys & rowids we have processed already
sourceKeysRemembered.clear();
targetRowidsRemembered.clear();
if(targetTableFilter!=null){
targetTableFilter.startQuery(session);
......@@ -142,21 +145,20 @@ public class MergeUsing extends Prepared {
sourceTableFilter.reset();
}
int countInputRows;
sourceQueryRowNumber = 0;
checkRights();
setCurrentRowNumber(0);
// process select query data for row creation
// process source select query data for row creation
ResultInterface rows = query.query(0);
countInputRows = 0;
targetTable.fire(session, evaluateTriggerMasks(), true);
targetTable.lock(session, true, false);
while (rows.next()) {
countInputRows++;
sourceQueryRowNumber++;
Value[] sourceRowValues = rows.currentRow();
Row sourceRow = new RowImpl(sourceRowValues,0);
ArrayList<Value> sourceKeyValuesList = new ArrayList<Value>();
setCurrentRowNumber(countInputRows);
setCurrentRowNumber(sourceQueryRowNumber);
// isolate the source row key columns values
for (int j = 0; j < sourceKeys.length; j++) {
......@@ -165,7 +167,7 @@ public class MergeUsing extends Prepared {
Value v = c.convert(sourceRowValues[j]);
sourceKeyValuesList.add(v);
} catch (DbException ex) {
throw setRow(ex, countInputRows, getSQL(sourceRowValues));
throw setRow(ex, sourceQueryRowNumber, getSQL(sourceRowValues));
}
}
......@@ -173,12 +175,12 @@ public class MergeUsing extends Prepared {
if(sourceKeysRemembered.containsKey(sourceKeyValuesList)){
throw DbException.get(ErrorCode.DUPLICATE_KEY_1, "Merge using ON column expression, duplicate values found:key columns"
+Arrays.asList(sourceKeys).toString()+":values:"+sourceKeyValuesList.toString()
+":from:"+sourceTableFilter.getTable()+":alias:"+sourceTableFilter.getTableAlias()+":current row number:"+countInputRows
+":from:"+sourceTableFilter.getTable()+":alias:"+sourceTableFilter.getTableAlias()+":current row number:"+sourceQueryRowNumber
+":conflicting row number:"+sourceKeysRemembered.get(sourceKeyValuesList));
}else{
// remember the source column values we have used before (they are the effective ON clause keys
// and should not be repeated
sourceKeysRemembered.put(sourceKeyValuesList,countInputRows);
sourceKeysRemembered.put(sourceKeyValuesList,sourceQueryRowNumber);
}
merge(sourceRow, sourceRowValues);
......@@ -223,8 +225,13 @@ public class MergeUsing extends Prepared {
// put the column values into the table filter
sourceTableFilter.set(sourceRow);
// Is the target row there already ?
boolean rowFound = isTargetRowFound();
// try and perform an update
int rowUpdateCount = 0;
if(rowFound){
if(updateCommand!=null){
rowUpdateCount += updateCommand.update();
}
......@@ -239,22 +246,59 @@ public class MergeUsing extends Prepared {
rowUpdateCount += deleteRowUpdateCount;
}
}
}
else
{
// if either updates do nothing, try an insert
if (rowUpdateCount == 0) {
rowUpdateCount+=addRowByCommandInsert(session);
rowUpdateCount += addRowByCommandInsert(session,sourceRow);
} else if (rowUpdateCount != 1) {
throw DbException.get(ErrorCode.DUPLICATE_KEY_1, "Duplicate key updated "+rowUpdateCount+" rows at once, only 1 expected:"+targetTable.getSQL());
throw DbException.get(ErrorCode.DUPLICATE_KEY_1, "Duplicate key inserted "+rowUpdateCount+" rows at once, only 1 expected:"+targetTable.getSQL());
}
}
countUpdatedRows+=rowUpdateCount;
}
private boolean isTargetRowFound(){
ResultInterface rows = targetMatchQuery.query(0);
int countTargetRowsFound = 0;
Value[] targetRowIdValue=null;
while (rows.next()) {
countTargetRowsFound++;
targetRowIdValue = rows.currentRow();
// throw and exception if we have processed this _ROWID_ before...
if(targetRowidsRemembered.containsKey(targetRowIdValue[0])){
throw DbException.get(ErrorCode.DUPLICATE_KEY_1, "Merge using ON column expression, duplicate _ROWID_ target record already updated, deleted or inserted:_ROWID_="
+targetRowIdValue[0].toString()
+":in:"+targetTableFilter.getTable()
+":conflicting source row number:"+targetRowidsRemembered.get(targetRowIdValue[0]));
}else{
// remember the source column values we have used before (they are the effective ON clause keys
// and should not be repeated
targetRowidsRemembered.put(targetRowIdValue[0],sourceQueryRowNumber);
}
}
rows.close();
if(countTargetRowsFound>1){
throw DbException.get(ErrorCode.DUPLICATE_KEY_1, "Duplicate key updated "+countTargetRowsFound+" rows at once, only 1 expected:_ROWID_="
+targetRowIdValue[0].toString()
+":in:"+targetTableFilter.getTable()
+":conflicting source row number:"+targetRowidsRemembered.get(targetRowIdValue[0]));
}
return countTargetRowsFound>0;
}
private int addRowByCommandInsert(Session session/*, Row newTargetRow*/) {
private int addRowByCommandInsert(Session session, Row sourceRow) {
int localCount = 0;
if(insertCommand!=null){
localCount += insertCommand.update();
if(!isTargetRowFound()){
throw DbException.get(ErrorCode.GENERAL_ERROR_1, "Expected to find key after row inserted, but none found. Insert does not match ON condition.:"+targetTable.getSQL()+":source row="+Arrays.asList(sourceRow.getValueList()));
}
}
return localCount;
}
......@@ -386,6 +430,14 @@ public class MergeUsing extends Prepared {
throw DbException.get(ErrorCode.SYNTAX_ERROR_1,
"At least UPDATE, DELETE or INSERT embedded statement must be supplied.");
}
// setup the targetMatchQuery - for detecting if the target row exists
Expression targetMatchCondition = targetMatchQuery.getCondition();
targetMatchCondition.addFilterConditions(sourceTableFilter, true);
targetMatchCondition.mapColumns(sourceTableFilter, 2);
targetMatchCondition = targetMatchCondition.optimize(session);
targetMatchCondition.createIndexConditions(session, sourceTableFilter);
targetMatchQuery.prepare();
}
private HashSet<Column> buildColumnListFromOnCondition(TableFilter anyTableFilter) {
......@@ -419,10 +471,19 @@ public class MergeUsing extends Prepared {
this.sourceTableFilter = sourceTableFilter;
}
public TableFilter getSourceTableFilter() {
return sourceTableFilter;
}
public void setOnCondition(Expression condition) {
this.onCondition = condition;
}
public Expression getOnCondition() {
return onCondition;
}
public Prepared getUpdateCommand() {
return updateCommand;
}
......@@ -470,13 +531,23 @@ public class MergeUsing extends Prepared {
public TableFilter getTargetTableFilter() {
return targetTableFilter;
}
public Table getTargetTable() {
return targetTable;
}
public void setTargetTable(Table targetTable) {
this.targetTable = targetTable;
}
public Select getTargetMatchQuery() {
return targetMatchQuery;
}
public void setTargetMatchQuery(Select targetMatchQuery) {
this.targetMatchQuery = targetMatchQuery;
}
// Prepared interface implementations
@Override
......@@ -493,4 +564,5 @@ public class MergeUsing extends Prepared {
public int getType() {
return CommandInterface.MERGE;
}
}
......@@ -168,6 +168,10 @@ public class Select extends Query {
}
}
public Expression getCondition() {
return condition;
}
private LazyResult queryGroupSorted(int columnCount, ResultTarget result) {
LazyResultGroupSorted lazyResult = new LazyResultGroupSorted(expressionArray, columnCount);
if (result == null) {
......
......@@ -183,6 +183,16 @@ public class TestMergeUsing extends TestBase {
3,
"No references to source columns found in ON clause"
);
// Insert does not insert correct values with respect to ON condition (inserts ID value above 100, instead)
testMergeUsingException(
"CREATE TABLE PARENT AS (SELECT 1 AS ID, 'Marcy'||X AS NAME FROM SYSTEM_RANGE(4,4) );"+
"CREATE TABLE SOURCE AS (SELECT X AS ID, 'Marcy'||X AS NAME FROM SYSTEM_RANGE(1,3) );",
"MERGE INTO PARENT USING SOURCE ON (PARENT.ID = SOURCE.ID) WHEN MATCHED THEN UPDATE SET PARENT.NAME = SOURCE.NAME||SOURCE.ID WHERE PARENT.ID = 2 DELETE WHERE PARENT.ID = 1 WHEN NOT MATCHED THEN INSERT (ID, NAME) VALUES (SOURCE.ID+100, SOURCE.NAME)",
GATHER_ORDERED_RESULTS_SQL,
"SELECT X AS ID, 'Marcy'||X AS NAME FROM SYSTEM_RANGE(4,4)",
1,
"Expected to find key after row inserted, but none found. Insert does not match ON condition."
);
}
/**
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论