Skip to content

Commit

Permalink
- **NEW** command type to create, maintain and manipulate a local-onl…
Browse files Browse the repository at this point in the history
…y relational database. One can use such facility

  to collect execution-bound data over multiple executions, or use the SQL capability to manipulate structured data set
  of any conceivable size.

Signed-off-by: automike <[email protected]>
  • Loading branch information
mikeliucc committed May 8, 2019
1 parent c93c1ac commit 1a160f4
Show file tree
Hide file tree
Showing 49 changed files with 765 additions and 417 deletions.
1 change: 1 addition & 0 deletions src/main/java/org/nexial/core/NexialConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ public final class NexialConst {
public static final String SQL_LINE_SEP = "\n";
public static final String CSV_ROW_SEP = "\n";
public static final String CSV_FIELD_DEIM = ",";
public static final String IMPORT_BUFFER_SIZE = registerSystemVariable(NAMESPACE + "rdbms.importBufferSize", 100);

// ws
public static final String WS_NAMESPACE = NAMESPACE + "ws.";
Expand Down
341 changes: 0 additions & 341 deletions src/main/java/org/nexial/core/plugins/db/RdbmsCommand.java

This file was deleted.

77 changes: 74 additions & 3 deletions src/main/java/org/nexial/core/plugins/db/SimpleExtractionDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import static org.nexial.core.NexialConst.*;
import static org.nexial.core.SystemVariables.getDefaultBool;
import static org.nexial.core.SystemVariables.getDefaultInt;

/**
* a <b>VERY</b> basic and stripped down version of data extraction via SQL statements or stored procedure. This class
Expand Down Expand Up @@ -185,8 +186,8 @@ protected JdbcOutcome executeSqls(List<SqlComponent> sqls) {
varName = context.replaceTokens(varName);
}

ConsoleUtils.log("Executing '" + StringUtils.defaultIfEmpty(varName, "UNNAMED QUERY") +
"' - " + query);
ConsoleUtils.log("Executing " + (StringUtils.isNotEmpty(varName) ? "'" + varName + "'" : "") + " - " +
query);

JdbcResult result = executeSql(query, null);

Expand Down Expand Up @@ -259,6 +260,76 @@ protected List<Map<String, String>> pack(List<Map<String, String>> results) {
return results;
}

protected JdbcResult importResults(SqlComponent sql, SimpleExtractionDao dao, TableSqlGenerator tableGenerator) {
long startTime = System.currentTimeMillis();

String query = sql.getSql();
JdbcResult result = new JdbcResult(query);

JdbcTemplate jdbc = getJdbcTemplate();
Integer rowsImported = jdbc.query(query, rs -> {
if (!rs.next()) {
result.setError("Unable to retrieve query resultset; Query execution possibly did not complete");
return -1;
}

// 1. generate DDL for target table
ResultSetMetaData metaData = rs.getMetaData();
String ddl = tableGenerator.generateSql(metaData);
JdbcResult ddlResult = dao.executeSql(ddl, null);
if (ddlResult.hasError()) {
result.setError("Failed to create table via '" + ddl + "': " + ddlResult.getError());
return -1;
}

// 2. generate DML to import data to target
int rowCount = 0;
int rowsAffected = 0;
int importBufferSize = context.getIntData(IMPORT_BUFFER_SIZE, getDefaultInt(IMPORT_BUFFER_SIZE));
StringBuilder error = new StringBuilder();
StringBuilder inserts = new StringBuilder();
int numOfColumn = metaData.getColumnCount();

do {
inserts.append("INSERT INTO ").append(tableGenerator.getTable()).append(" VALUES (");
for (int i = 1; i <= numOfColumn; i++) {
String data = rs.getString(i);
if (rs.wasNull()) {
data = "NULL";
} else if (tableGenerator.isTextColumnType(metaData.getColumnType(i))) {
data = "'" + StringUtils.replace(data, "'", "''") + "'";
}
inserts.append(data);
if (i < numOfColumn) { inserts.append(","); }
}

inserts.append(");\n");

rowCount++;

if (rowCount % importBufferSize == 0) {
JdbcOutcome insertResults = dao.executeSqls(SqlComponent.toList(inserts.toString()));
rowsAffected += insertResults.getRowsAffected();
if (result.hasError()) { error.append(result.getError()).append("\n"); }
inserts = new StringBuilder();
}
} while (rs.next());

JdbcOutcome insertResults = dao.executeSqls(SqlComponent.toList(inserts.toString()));
rowsAffected += insertResults.getRowsAffected();
if (result.hasError()) { error.append(result.getError()).append("\n"); }

if (error.length() > 0) { result.setError(error.toString()); }

return rowsAffected;
});

result.setRowCount(rowsImported == null ? -1 : rowsImported);
result.setTiming(startTime);

return result;
}

protected void setAutoCommit(Boolean autoCommit) { this.autoCommit = autoCommit; }

protected Boolean isAutoCommit() {
Expand Down Expand Up @@ -320,7 +391,7 @@ protected <T extends JdbcResult> T resultToListOfMap(ResultSet rs, T result) thr
List<Map<String, String>> rows = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();

// recycle through all rows
// cycle through all rows
do {
Map<String, String> row = new LinkedHashMap<>();

Expand Down
150 changes: 150 additions & 0 deletions src/main/kotlin/org/nexial/core/plugins/db/LocalDbCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.nexial.core.plugins.db

import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.nexial.commons.utils.RegexUtils
import org.nexial.commons.utils.TextUtils
import org.nexial.core.NexialConst.DAO_PREFIX
import org.nexial.core.model.ExecutionContext
import org.nexial.core.model.StepResult
import org.nexial.core.plugins.base.BaseCommand
import org.nexial.core.utils.CheckUtils.requiresNotBlank
import org.nexial.core.utils.CheckUtils.requiresValidVariableName
import org.nexial.core.utils.OutputFileUtils
import java.io.File
import java.sql.SQLException

/**
* The concept of "localdb" is to maintain a database within a Nexial installation, with its data stay persisted and
* intact across executions. It is "local" - meaning, it does not share with a remote Nexial instance and it
* cannot be connected via a remote Nexial instance. The purpose of a local-only database can be manifold:
* 1. store and compare data over multiple executions
* 2. tally and summary over multiple executions
* 3. as an intermediate query engine to further manipulate data
* 4. data collection over discrete automation
*/
class LocalDbCommand : BaseCommand() {

lateinit var dbName: String
lateinit var dbFile: String
lateinit var rdbms: RdbmsCommand
lateinit var connectionProps: MutableMap<String, String>
lateinit var dao: SimpleExtractionDao

override fun getTarget() = "localdb"

override fun init(context: ExecutionContext) {
super.init(context)

dbFile = context.replaceTokens(dbFile)
File(dbFile).parentFile.mkdirs()

initConnection()
}

private fun initConnection() {
connectionProps.forEach { (key, value) -> context.setData(key, value); }
rdbms.init(context)
dao = rdbms.dataAccess.resolveDao(dbName)
context.setData(DAO_PREFIX + dbName, dao)
}

fun purge(`var`: String): StepResult {
requiresValidVariableName(`var`)

try {
dao.dataSource?.connection?.close()
} catch (e: SQLException) {
// probably not yet connected.. ignore
}

val deleted = FileUtils.deleteQuietly(File(dbFile))
context.setData(`var`, deleted)

initConnection()

return StepResult(deleted,
"localdb ${if (deleted) "purged" else "DID NOT purged; try manual delete of localdb file"}",
null)
}

fun runSQLs(`var`: String, sqls: String): StepResult = rdbms.runSQLs(`var`, dbName, sqls)

fun dropTables(`var`: String, tables: String): StepResult {
requiresValidVariableName(`var`)
requiresNotBlank(tables, "invalid table(s)", tables)

val sqls = StringUtils.split(tables, context.textDelim).map { table -> "DROP TABLE $table;\n" }
return rdbms.runSQL(`var`, dbName, TextUtils.toString(sqls, "\n", "") + "\nVACUUM;")
}

fun cloneTable(`var`: String, source: String, target: String): StepResult {
requiresValidVariableName(`var`)
requiresNotBlank(source, "invalid source table", source)
requiresNotBlank(target, "invalid target table", target)

// 1. find DDL SQL for `source`
val sql = "SELECT sql FROM SQLITE_MASTER WHERE TYPE='table' AND lower(name)='${source.toLowerCase()}';"
val result = rdbms.dataAccess.execute(sql, dao) ?: return StepResult.fail(
"FAILED TO DETERMINE DDL SQL for $source: no result found")

if (result.isEmpty) return StepResult.fail("Source table '$source' not found in localdb")
if (result.hasError()) return StepResult.fail("Source table '$source' not found in localdb: ${result.error}")

// 2. convert to DDL for `target`
if (result.data[0]["sql"] == null) return StepResult.fail(
"Unable to determine the CREATE SQL for source table '$source'")

val ddl = RegexUtils.replace(result.data[0]["sql"], "(CREATE TABLE\\s+)([A-Za-z0-9_]+)(.+)", "\$1$target\$3")

// 3. execute
val createResult = rdbms.runSQL(`var`, dbName, ddl)
if (createResult.isError) return createResult

// 4. copy data
return rdbms.runSQLs(`var`, dbName, "INSERT INTO $target SELECT * FROM $source;")
}

fun importRecords(`var`: String, sourceDb: String, sql: String, table: String): StepResult {
requiresValidVariableName(`var`)
requiresNotBlank(sourceDb, "invalid source database connection name", sourceDb)
requiresNotBlank(sql, "invalid SQL", sql)
requiresNotBlank(table, "invalid target table name", table)

val query = SqlComponent(OutputFileUtils.resolveRawContent(sql, context))
if (!query.type.hasResultset()) return StepResult.fail("SQL '$sql' MUST return resultset")

val sourceDao = rdbms.dataAccess.resolveDao(sourceDb) ?: return StepResult.fail(
"Unable to connection to source database '$sourceDb'")

context.setData(`var`, sourceDao.importResults(query, dao, SqliteTableSqlGenerator(table)))
return StepResult.success("Query result successfully imported from '$sourceDb' to localdb '$table'")
}

fun exportCSV(sql: String, output: String): StepResult = rdbms.saveResult(dbName, sql, output)

/*
importJSON(var,json,table)
importXML(var,xml,table)
importCSV(var,csv,table)
exportXML(sql,output)
exportJSON(sql,output)
*/
}
Loading

0 comments on commit 1a160f4

Please sign in to comment.