mirror of https://github.com/apache/nifi.git
NIFI-6370: Allow multiple SQL statements in PutDatabaseRecord
This closes #3528. Signed-off-by: Koji Kawamura <ijokarumawak@apache.org>
This commit is contained in:
parent
f15332ff87
commit
05f3cadee8
|
@ -239,6 +239,17 @@ public class PutDatabaseRecord extends AbstractSessionFactoryProcessor {
|
||||||
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
static final PropertyDescriptor ALLOW_MULTIPLE_STATEMENTS = new PropertyDescriptor.Builder()
|
||||||
|
.name("put-db-record-allow-multiple-statements")
|
||||||
|
.displayName("Allow Multiple SQL Statements")
|
||||||
|
.description("If the Statement Type is 'SQL' (as set in the statement.type attribute), this field indicates whether to split the field value by a semicolon and execute each statement "
|
||||||
|
+ "separately. If any statement causes an error, the entire set of statements will be rolled back. If the Statement Type is not 'SQL', this field is ignored.")
|
||||||
|
.addValidator(StandardValidators.BOOLEAN_VALIDATOR)
|
||||||
|
.required(true)
|
||||||
|
.allowableValues("true", "false")
|
||||||
|
.defaultValue("false")
|
||||||
|
.build();
|
||||||
|
|
||||||
static final PropertyDescriptor QUOTED_IDENTIFIERS = new PropertyDescriptor.Builder()
|
static final PropertyDescriptor QUOTED_IDENTIFIERS = new PropertyDescriptor.Builder()
|
||||||
.name("put-db-record-quoted-identifiers")
|
.name("put-db-record-quoted-identifiers")
|
||||||
.displayName("Quote Column Identifiers")
|
.displayName("Quote Column Identifiers")
|
||||||
|
@ -309,6 +320,7 @@ public class PutDatabaseRecord extends AbstractSessionFactoryProcessor {
|
||||||
pds.add(UNMATCHED_COLUMN_BEHAVIOR);
|
pds.add(UNMATCHED_COLUMN_BEHAVIOR);
|
||||||
pds.add(UPDATE_KEYS);
|
pds.add(UPDATE_KEYS);
|
||||||
pds.add(FIELD_CONTAINING_SQL);
|
pds.add(FIELD_CONTAINING_SQL);
|
||||||
|
pds.add(ALLOW_MULTIPLE_STATEMENTS);
|
||||||
pds.add(QUOTED_IDENTIFIERS);
|
pds.add(QUOTED_IDENTIFIERS);
|
||||||
pds.add(QUOTED_TABLE_IDENTIFIER);
|
pds.add(QUOTED_TABLE_IDENTIFIER);
|
||||||
pds.add(QUERY_TIMEOUT);
|
pds.add(QUERY_TIMEOUT);
|
||||||
|
@ -404,7 +416,15 @@ public class PutDatabaseRecord extends AbstractSessionFactoryProcessor {
|
||||||
|
|
||||||
getLogger().warn("Failed to process {} due to {}", new Object[]{inputFlowFile, e}, e);
|
getLogger().warn("Failed to process {} due to {}", new Object[]{inputFlowFile, e}, e);
|
||||||
|
|
||||||
if (e instanceof BatchUpdateException) {
|
// Check if there was a BatchUpdateException or if multiple SQL statements were being executed and one failed
|
||||||
|
final String statementTypeProperty = context.getProperty(STATEMENT_TYPE).getValue();
|
||||||
|
String statementType = statementTypeProperty;
|
||||||
|
if (USE_ATTR_TYPE.equals(statementTypeProperty)) {
|
||||||
|
statementType = inputFlowFile.getAttribute(STATEMENT_TYPE_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof BatchUpdateException
|
||||||
|
|| (SQL_TYPE.equalsIgnoreCase(statementType) && context.getProperty(ALLOW_MULTIPLE_STATEMENTS).asBoolean())) {
|
||||||
try {
|
try {
|
||||||
// Although process session will move forward in order to route the failed FlowFile,
|
// Although process session will move forward in order to route the failed FlowFile,
|
||||||
// database transaction should be rolled back to avoid partial batch update.
|
// database transaction should be rolled back to avoid partial batch update.
|
||||||
|
@ -567,9 +587,17 @@ public class PutDatabaseRecord extends AbstractSessionFactoryProcessor {
|
||||||
throw new MalformedRecordException(format("Record had no (or null) value for Field Containing SQL: %s, FlowFile %s", sqlField, flowFile));
|
throw new MalformedRecordException(format("Record had no (or null) value for Field Containing SQL: %s, FlowFile %s", sqlField, flowFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the statement as-is
|
// Execute the statement(s) as-is
|
||||||
|
if (context.getProperty(ALLOW_MULTIPLE_STATEMENTS).asBoolean()) {
|
||||||
|
String regex = "(?<!\\\\);";
|
||||||
|
String[] sqlStatements = ((String) sql).split(regex);
|
||||||
|
for (String statement : sqlStatements) {
|
||||||
|
s.execute(statement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
s.execute((String) sql);
|
s.execute((String) sql);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result.routeTo(flowFile, REL_SUCCESS);
|
result.routeTo(flowFile, REL_SUCCESS);
|
||||||
session.getProvenanceReporter().send(flowFile, functionContext.jdbcUrl);
|
session.getProvenanceReporter().send(flowFile, functionContext.jdbcUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ import static org.junit.Assert.assertTrue
|
||||||
import static org.junit.Assert.fail
|
import static org.junit.Assert.fail
|
||||||
import static org.mockito.Matchers.anyMap
|
import static org.mockito.Matchers.anyMap
|
||||||
import static org.mockito.Mockito.doAnswer
|
import static org.mockito.Mockito.doAnswer
|
||||||
import static org.mockito.Mockito.only
|
|
||||||
import static org.mockito.Mockito.spy
|
import static org.mockito.Mockito.spy
|
||||||
import static org.mockito.Mockito.times
|
import static org.mockito.Mockito.times
|
||||||
import static org.mockito.Mockito.verify
|
import static org.mockito.Mockito.verify
|
||||||
|
@ -413,6 +412,82 @@ class TestPutDatabaseRecord {
|
||||||
conn.close()
|
conn.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleInsertsViaSqlStatementType() throws InitializationException, ProcessException, SQLException, IOException {
|
||||||
|
recreateTable("PERSONS", createPersons)
|
||||||
|
final MockRecordParser parser = new MockRecordParser()
|
||||||
|
runner.addControllerService("parser", parser)
|
||||||
|
runner.enableControllerService(parser)
|
||||||
|
|
||||||
|
parser.addSchemaField("sql", RecordFieldType.STRING)
|
||||||
|
|
||||||
|
parser.addRecord('''INSERT INTO PERSONS (id, name, code) VALUES (1, 'rec1',101);INSERT INTO PERSONS (id, name, code) VALUES (2, 'rec2',102)''')
|
||||||
|
|
||||||
|
runner.setProperty(PutDatabaseRecord.RECORD_READER_FACTORY, 'parser')
|
||||||
|
runner.setProperty(PutDatabaseRecord.STATEMENT_TYPE, PutDatabaseRecord.USE_ATTR_TYPE)
|
||||||
|
runner.setProperty(PutDatabaseRecord.TABLE_NAME, 'PERSONS')
|
||||||
|
runner.setProperty(PutDatabaseRecord.FIELD_CONTAINING_SQL, 'sql')
|
||||||
|
runner.setProperty(PutDatabaseRecord.ALLOW_MULTIPLE_STATEMENTS, 'true')
|
||||||
|
|
||||||
|
def attrs = [:]
|
||||||
|
attrs[PutDatabaseRecord.STATEMENT_TYPE_ATTRIBUTE] = 'sql'
|
||||||
|
runner.enqueue(new byte[0], attrs)
|
||||||
|
runner.run()
|
||||||
|
|
||||||
|
runner.assertTransferCount(PutDatabaseRecord.REL_SUCCESS, 1)
|
||||||
|
final Connection conn = dbcp.getConnection()
|
||||||
|
final Statement stmt = conn.createStatement()
|
||||||
|
final ResultSet rs = stmt.executeQuery('SELECT * FROM PERSONS')
|
||||||
|
assertTrue(rs.next())
|
||||||
|
assertEquals(1, rs.getInt(1))
|
||||||
|
assertEquals('rec1', rs.getString(2))
|
||||||
|
assertEquals(101, rs.getInt(3))
|
||||||
|
assertTrue(rs.next())
|
||||||
|
assertEquals(2, rs.getInt(1))
|
||||||
|
assertEquals('rec2', rs.getString(2))
|
||||||
|
assertEquals(102, rs.getInt(3))
|
||||||
|
assertFalse(rs.next())
|
||||||
|
|
||||||
|
stmt.close()
|
||||||
|
conn.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleInsertsViaSqlStatementTypeBadSQL() throws InitializationException, ProcessException, SQLException, IOException {
|
||||||
|
recreateTable("PERSONS", createPersons)
|
||||||
|
final MockRecordParser parser = new MockRecordParser()
|
||||||
|
runner.addControllerService("parser", parser)
|
||||||
|
runner.enableControllerService(parser)
|
||||||
|
|
||||||
|
parser.addSchemaField("sql", RecordFieldType.STRING)
|
||||||
|
|
||||||
|
parser.addRecord('''INSERT INTO PERSONS (id, name, code) VALUES (1, 'rec1',101);
|
||||||
|
INSERT INTO PERSONS (id, name, code) VALUES (2, 'rec2',102);
|
||||||
|
INSERT INTO PERSONS2 (id, name, code) VALUES (2, 'rec2',102);''')
|
||||||
|
|
||||||
|
runner.setProperty(PutDatabaseRecord.RECORD_READER_FACTORY, 'parser')
|
||||||
|
runner.setProperty(PutDatabaseRecord.STATEMENT_TYPE, PutDatabaseRecord.USE_ATTR_TYPE)
|
||||||
|
runner.setProperty(PutDatabaseRecord.TABLE_NAME, 'PERSONS')
|
||||||
|
runner.setProperty(PutDatabaseRecord.FIELD_CONTAINING_SQL, 'sql')
|
||||||
|
runner.setProperty(PutDatabaseRecord.ALLOW_MULTIPLE_STATEMENTS, 'true')
|
||||||
|
|
||||||
|
def attrs = [:]
|
||||||
|
attrs[PutDatabaseRecord.STATEMENT_TYPE_ATTRIBUTE] = 'sql'
|
||||||
|
runner.enqueue(new byte[0], attrs)
|
||||||
|
runner.run()
|
||||||
|
|
||||||
|
runner.assertTransferCount(PutDatabaseRecord.REL_SUCCESS, 0)
|
||||||
|
runner.assertTransferCount(PutDatabaseRecord.REL_FAILURE, 1)
|
||||||
|
final Connection conn = dbcp.getConnection()
|
||||||
|
final Statement stmt = conn.createStatement()
|
||||||
|
final ResultSet rs = stmt.executeQuery('SELECT * FROM PERSONS')
|
||||||
|
// The first two legitimate statements should have been rolled back
|
||||||
|
assertFalse(rs.next())
|
||||||
|
|
||||||
|
stmt.close()
|
||||||
|
conn.close()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSqlStatementTypeNoValue() throws InitializationException, ProcessException, SQLException, IOException {
|
void testSqlStatementTypeNoValue() throws InitializationException, ProcessException, SQLException, IOException {
|
||||||
recreateTable("PERSONS", createPersons)
|
recreateTable("PERSONS", createPersons)
|
||||||
|
|
Loading…
Reference in New Issue