This is an automated email from the ASF dual-hosted git repository.
adebreceni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new bfecf29 MINIFICPP-1541 Mock SQL driver dependant tests MINIFICPP-1541
Add option to use SQLite driver Fix regression due to new semantics of split
function Fix split string utils Create parser for simple SQL WHERE conditions
Add switch for real ODBC driver in windows build script Add docker tests to SQL
extension
bfecf29 is described below
commit bfecf29e3c56c6ee9a1915f836baa8ad2d75045a
Author: Gabor Gyimesi <[email protected]>
AuthorDate: Fri Jul 30 14:55:31 2021 +0200
MINIFICPP-1541 Mock SQL driver dependant tests
MINIFICPP-1541 Add option to use SQLite driver
Fix regression due to new semantics of split function
Fix split string utils
Create parser for simple SQL WHERE conditions
Add switch for real ODBC driver in windows build script
Add docker tests to SQL extension
Signed-off-by: Adam Debreceni <[email protected]>
This closes #1073
---
.github/workflows/ci.yml | 12 +-
CMakeLists.txt | 4 +
Windows.md | 1 +
.../integration/MiNiFi_integration_test_driver.py | 6 +
docker/test/integration/features/sql.feature | 45 +++
.../integration/minifi/controllers/ODBCService.py | 11 +
.../{core => controllers}/SSLContextService.py | 2 +-
.../integration/minifi/controllers/__init__.py | 0
.../integration/minifi/core/DockerTestCluster.py | 11 +
.../minifi/core/SingleNodeDockerCluster.py | 49 +++
.../integration/minifi/processors/ExecuteSQL.py | 9 +
.../test/integration/minifi/processors/PutSQL.py | 9 +
.../minifi/processors/QueryDatabaseTable.py | 8 +
.../minifi/processors/UpdateAttribute.py | 9 +
docker/test/integration/steps/steps.py | 29 +-
extensions/pdh/PDHCounters.cpp | 6 +-
extensions/sql/data/DatabaseConnectors.h | 86 ++---
extensions/sql/data/SQLRowsetProcessor.cpp | 50 ++-
extensions/sql/data/SQLRowsetProcessor.h | 10 +-
extensions/sql/data/SociConnectors.cpp | 181 +++++++++
extensions/sql/data/SociConnectors.h | 120 ++++++
extensions/sql/data/Utils.cpp | 5 +-
extensions/sql/processors/ExecuteSQL.cpp | 2 +-
extensions/sql/processors/QueryDatabaseTable.cpp | 2 +-
extensions/sql/services/DatabaseService.h | 12 +-
extensions/sql/services/ODBCConnector.h | 54 +--
libminifi/include/utils/StringUtils.h | 2 +
libminifi/src/utils/StringUtils.cpp | 43 ++-
libminifi/src/utils/tls/DistinguishedName.cpp | 4 +-
libminifi/test/sql-tests/CMakeLists.txt | 9 +-
libminifi/test/sql-tests/PutSQLTests.cpp | 3 +-
libminifi/test/sql-tests/SQLTestController.h | 36 +-
libminifi/test/sql-tests/SQLTestPlan.h | 9 +-
libminifi/test/sql-tests/mocks/MockConnectors.cpp | 416 +++++++++++++++++++++
libminifi/test/sql-tests/mocks/MockConnectors.h | 173 +++++++++
libminifi/test/sql-tests/mocks/MockODBCService.h | 67 ++++
libminifi/test/unit/StringUtilsTests.cpp | 64 +++-
win_build_vs.bat | 4 +-
38 files changed, 1362 insertions(+), 201 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e0fff2b..0008755 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,7 +36,7 @@ jobs:
export LDFLAGS="-L/usr/local/opt/flex/lib"
export CPPFLAGS="-I/usr/local/opt/flex/include"
# CPPFLAGS are not recognized by cmake, so we have to force them to
CFLAGS and CXXFLAGS to have flex 2.6 working
- ./bootstrap.sh -e -t && cd build && cmake
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="${CPPFLAGS} ${CFLAGS}"
-DCMAKE_CXX_FLAGS="${CPPFLAGS} ${CXXFLAGS}" -DENABLE_LUA_SCRIPTING=1
-DENABLE_SQL=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES=OFF
-DSTRICT_GSL_CHECKS=AUDIT -DFAIL_ON_WARNINGS=OFF .. && cmake --build .
--parallel 4
+ ./bootstrap.sh -e -t && cd build && cmake
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="${CPPFLAGS} ${CFLAGS}"
-DCMAKE_CXX_FLAGS="${CPPFLAGS} ${CXXFLAGS}" -DENABLE_LUA_SCRIPTING=1
-DENABLE_SQL=ON -DUSE_REAL_ODBC_TEST_DRIVER=ON -DCMAKE_VERBOSE_MAKEFILE=ON
-DCMAKE_RULE_MESSAGES=OFF -DSTRICT_GSL_CHECKS=AUDIT -DFAIL_ON_WARNINGS=ON .. &&
cmake --build . --parallel 4
- name: test
run: cd build && make test ARGS="--timeout 300 -j4 --output-on-failure"
- name: linter
@@ -79,7 +79,7 @@ jobs:
run: |
PATH %PATH%;C:\Program Files (x86)\Windows
Kits\10\bin\10.0.19041.0\x64
PATH %PATH%;C:\Program Files (x86)\Microsoft Visual
Studio\2019\Enterprise\MSBuild\Current\Bin\Roslyn
- win_build_vs.bat build /64 /CI /S /A /PDH /K /L /R /Z /N /O
+ win_build_vs.bat build /64 /CI /S /A /PDH /K /L /R /Z /N /O /RO
shell: cmd
- name: test
run: cd build && ctest --timeout 300 --parallel 8 -C Release
--output-on-failure
@@ -106,7 +106,6 @@ jobs:
run: |
sudo apt update
sudo apt install -y ccache libfl-dev libpcap-dev libboost-all-dev
openjdk-8-jdk maven libusb-1.0-0-dev libpng-dev libgps-dev libsqliteodbc flake8
- sudo ln -s /usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so
/usr/lib/x86_64-linux-gnu/libsqlite3odbc.so
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
echo -e "127.0.0.1\t$HOSTNAME" | sudo tee -a /etc/hosts > /dev/null
- name: build
@@ -146,8 +145,7 @@ jobs:
- id: install_deps
run: |
sudo apt update
- sudo apt install -y ccache libfl-dev libpcap-dev libboost-all-dev
openjdk-8-jdk maven libusb-1.0-0-dev libpng-dev libgps-dev libsqliteodbc
- sudo ln -s /usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so
/usr/lib/x86_64-linux-gnu/libsqlite3odbc.so
+ sudo apt install -y ccache libfl-dev libpcap-dev libboost-all-dev
openjdk-8-jdk maven libusb-1.0-0-dev libpng-dev libgps-dev
echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
echo -e "127.0.0.1\t$HOSTNAME" | sudo tee -a /etc/hosts > /dev/null
- name: build
@@ -156,7 +154,7 @@ jobs:
cd build
export CC=clang
export CXX=clang++
- cmake -DUSE_SHARED_LIBS= -DCMAKE_BUILD_TYPE=Release
-DENABLE_NANOFI=ON -DENABLE_JNI=ON -DENABLE_SENSORS=ON -DENABLE_OPENWSMAN=ON
-DENABLE_OPENCV=ON -DENABLE_MQTT=ON -DENABLE_GPS=ON -DENABLE_USB_CAMERA=ON
-DENABLE_LIBRDKAFKA=ON -DENABLE_OPC=ON -DENABLE_SFTP=ON -DENABLE_COAP=ON
-DENABLE_PYTHON=ON -DENABLE_SQL=ON -DENABLE_AWS=ON -DSTRICT_GSL_CHECKS=AUDIT
-DFAIL_ON_WARNINGS=ON ..
+ cmake -DUSE_SHARED_LIBS= -DCMAKE_BUILD_TYPE=Release
-DENABLE_NANOFI=ON -DENABLE_JNI=ON -DENABLE_SENSORS=ON -DENABLE_OPENWSMAN=ON
-DENABLE_OPENCV=ON -DENABLE_MQTT=ON -DENABLE_GPS=ON -DENABLE_USB_CAMERA=ON
-DENABLE_LIBRDKAFKA=ON -DENABLE_OPC=ON -DENABLE_SFTP=ON -DENABLE_COAP=ON
-DENABLE_PYTHON=ON -DENABLE_SQL=ON -DENABLE_AWS=ON -DENABLE_AZURE=ON
-DSTRICT_GSL_CHECKS=AUDIT -DFAIL_ON_WARNINGS=ON ..
cmake --build . --parallel $(nproc)
- name: test
run: cd build && make test ARGS="--timeout 300 -j8 --output-on-failure"
@@ -266,7 +264,7 @@ jobs:
run: |
./bootstrap.sh -e -t
cd build
- cmake -DUSE_SHARED_LIBS= -DSTRICT_GSL_CHECKS=AUDIT -DENABLE_JNI=OFF
-DDISABLE_JEMALLOC=ON -DDISABLE_SCRIPTING=ON -DENABLE_AWS=ON
-DENABLE_LIBRDKAFKA=ON -DENABLE_AZURE=ON ..
+ cmake -DUSE_SHARED_LIBS= -DSTRICT_GSL_CHECKS=AUDIT -DENABLE_JNI=OFF
-DDISABLE_JEMALLOC=ON -DDISABLE_SCRIPTING=ON -DENABLE_AWS=ON
-DENABLE_LIBRDKAFKA=ON -DENABLE_AZURE=ON -DENABLE_SQL=ON ..
make docker
- id: install_deps
run: |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 21a4c3d..82ffde6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -60,6 +60,10 @@ option(DISABLE_CURL "Disables libCurl Properties." OFF)
option(USE_GOLD_LINKER "Use Gold Linker" OFF)
option(INSTALLER_MERGE_MODULES "Creates installer with merge modules" OFF)
option(FAIL_ON_WARNINGS "Treat warnings as errors" OFF)
+option(USE_REAL_ODBC_TEST_DRIVER "Use SQLite ODBC driver in SQL extenstion
unit tests instead of a mock database" OFF)
+if (USE_REAL_ODBC_TEST_DRIVER)
+ add_definitions("-DUSE_REAL_ODBC_TEST_DRIVER")
+endif()
# This is needed for ninja:
# By default, neither Clang or GCC will add ANSI-formatted colors to your
output if they detect
# the output medium is not a terminal. This means no coloring when using a
generator
diff --git a/Windows.md b/Windows.md
index cbb97a9..73f452b 100644
--- a/Windows.md
+++ b/Windows.md
@@ -67,6 +67,7 @@ After the build directory it will take optional parameters
modifying the CMake c
| /L | Enables Linter |
| /O | Enables OpenCV |
| /PDH | Enables Performance Monitor |
+| /RO | Use real ODBC driver in tests instead of mock SQL driver |
| /M | Creates installer with merge modules |
| /64 | Creates 64-bit build instead of a 32-bit one |
| /D | Builds RelWithDebInfo build instead of Release |
diff --git a/docker/test/integration/MiNiFi_integration_test_driver.py
b/docker/test/integration/MiNiFi_integration_test_driver.py
index 43178d6..bc346e7 100644
--- a/docker/test/integration/MiNiFi_integration_test_driver.py
+++ b/docker/test/integration/MiNiFi_integration_test_driver.py
@@ -120,6 +120,8 @@ class MiNiFi_integration_test():
startup_success = cluster.wait_for_app_logs("Started
S3MockApplication", 120)
elif cluster.get_engine() == "azure-storage-server":
startup_success = cluster.wait_for_app_logs("Azurite Queue service
is successfully listening at", 120)
+ elif cluster.get_engine() == "postgresql-server":
+ startup_success = cluster.wait_for_app_logs("database system is
ready to accept connections", 120)
if not startup_success:
logging.error("Cluster startup failed for %s", cluster.get_name())
cluster.log_app_output()
@@ -266,3 +268,7 @@ class MiNiFi_integration_test():
def wait_for_kafka_consumer_to_be_registered(self, cluster_name):
cluster = self.acquire_cluster(cluster_name)
assert cluster.wait_for_kafka_consumer_to_be_registered()
+
+ def check_query_results(self, cluster_name, query, number_of_rows,
timeout_seconds):
+ cluster = self.acquire_cluster(cluster_name)
+ assert cluster.check_query_results(query, number_of_rows,
timeout_seconds)
diff --git a/docker/test/integration/features/sql.feature
b/docker/test/integration/features/sql.feature
new file mode 100644
index 0000000..0704fde
--- /dev/null
+++ b/docker/test/integration/features/sql.feature
@@ -0,0 +1,45 @@
+Feature: Execuring SQL operations from MiNiFi-C++
+ As a user of MiNiFi
+ I need to have ExecuteSQL, QueryDatabaseTable and PutSQL processors
+
+ Background:
+ Given the content of "/tmp/output" is monitored
+
+ Scenario: A MiNiFi instance can insert data to test table with PutSQL
processor
+ Given a GenerateFlowFile processor with the "File Size" property set to
"0B"
+ And a UpdateAttribute processor with the "sql.args.1.value" property set
to "42"
+ And the "sql.args.2.value" property of the UpdateAttribute processor is
set to "pineapple"
+ And a PutSQL processor with the "SQL Statement" property set to "INSERT
INTO test_table (int_col, text_col) VALUES (?, ?)"
+ And the "success" relationship of the GenerateFlowFile processor is
connected to the UpdateAttribute
+ And the "success" relationship of the UpdateAttribute processor is
connected to the PutSQL
+ And an ODBCService is setup up for PutSQL with the name "ODBCService" and
connection string "Driver={PostgreSQL
ANSI};Server=postgresql-server;Port=5432;Database=postgres;Uid=postgres;Pwd=password;"
+ And a PostgreSQL server "postgresql" is set up
+ When all instances start up
+ Then the query "SELECT * FROM test_table WHERE int_col = 42" returns 1
rows in less than 120 seconds on the "postgresql" PostgreSQL server
+
+ Scenario: A MiNiFi instance can query to test table with ExecuteSQL processor
+ Given a GenerateFlowFile processor with the "File Size" property set to
"0B"
+ And a UpdateAttribute processor with the "sql.args.1.value" property set
to "apple"
+ And the "sql.args.2.value" property of the UpdateAttribute processor is
set to "banana"
+ And a ExecuteSQL processor with the "SQL select query" property set to
"SELECT * FROM test_table WHERE text_col = ? OR text_col = ? ORDER BY int_col
DESC"
+ And the "Output Format" property of the ExecuteSQL processor is set to
"JSON"
+ And a PutFile processor with the "Directory" property set to "/tmp/output"
+ And the "success" relationship of the GenerateFlowFile processor is
connected to the UpdateAttribute
+ And the "success" relationship of the UpdateAttribute processor is
connected to the ExecuteSQL
+ And the "success" relationship of the ExecuteSQL processor is connected to
the PutFile
+ And an ODBCService is setup up for ExecuteSQL with the name "ODBCService"
and connection string "Driver={PostgreSQL
ANSI};Server=postgresql-server;Port=5432;Database=postgres;Uid=postgres;Pwd=password;"
+ And a PostgreSQL server "postgresql" is set up
+ When all instances start up
+ Then at least one flowfile with the content
'[{"int_col":2,"text_col":"banana"},{"int_col":1,"text_col":"apple"}]' is
placed in the monitored directory in less than 120 seconds
+
+ Scenario: A MiNiFi instance can query to test table with QueryDatabaseTable
processor
+ Given a QueryDatabaseTable processor with the "Table Name" property set to
"test_table"
+ And the "Columns to Return" property of the QueryDatabaseTable processor
is set to "text_col"
+ And the "Where Clause" property of the QueryDatabaseTable processor is set
to "int_col = 1"
+ And the "Output Format" property of the QueryDatabaseTable processor is
set to "JSON"
+ And a PutFile processor with the "Directory" property set to "/tmp/output"
+ And the "success" relationship of the QueryDatabaseTable processor is
connected to the PutFile
+ And an ODBCService is setup up for QueryDatabaseTable with the name
"ODBCService" and connection string "Driver={PostgreSQL
ANSI};Server=postgresql-server;Port=5432;Database=postgres;Uid=postgres;Pwd=password;"
+ And a PostgreSQL server "postgresql" is set up
+ When all instances start up
+ Then at least one flowfile with the content '[{"text_col":"apple"}]' is
placed in the monitored directory in less than 120 seconds
diff --git a/docker/test/integration/minifi/controllers/ODBCService.py
b/docker/test/integration/minifi/controllers/ODBCService.py
new file mode 100644
index 0000000..b7db78d
--- /dev/null
+++ b/docker/test/integration/minifi/controllers/ODBCService.py
@@ -0,0 +1,11 @@
+from ..core.ControllerService import ControllerService
+
+
+class ODBCService(ControllerService):
+ def __init__(self, name=None, connection_string=None):
+ super(ODBCService, self).__init__(name=name)
+
+ self.service_class = 'ODBCService'
+
+ if connection_string is not None:
+ self.properties['Connection String'] = connection_string
diff --git a/docker/test/integration/minifi/core/SSLContextService.py
b/docker/test/integration/minifi/controllers/SSLContextService.py
similarity index 89%
rename from docker/test/integration/minifi/core/SSLContextService.py
rename to docker/test/integration/minifi/controllers/SSLContextService.py
index 8e8dc40..2f8bdc1 100644
--- a/docker/test/integration/minifi/core/SSLContextService.py
+++ b/docker/test/integration/minifi/controllers/SSLContextService.py
@@ -1,4 +1,4 @@
-from .ControllerService import ControllerService
+from ..core.ControllerService import ControllerService
class SSLContextService(ControllerService):
diff --git a/docker/test/integration/minifi/controllers/__init__.py
b/docker/test/integration/minifi/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/docker/test/integration/minifi/core/DockerTestCluster.py
b/docker/test/integration/minifi/core/DockerTestCluster.py
index c5c4e01..7d40595 100644
--- a/docker/test/integration/minifi/core/DockerTestCluster.py
+++ b/docker/test/integration/minifi/core/DockerTestCluster.py
@@ -134,6 +134,17 @@ class DockerTestCluster(SingleNodeDockerCluster):
ls_result = subprocess.check_output(["docker", "exec", "s3-server",
"ls", s3_mock_dir + "/test_bucket/"]).decode(self.get_stdout_encoding())
return not ls_result
+ def query_postgres_server(self, query, number_of_rows):
+ return str(number_of_rows) + " rows" in
subprocess.check_output(["docker", "exec", "postgresql-server", "psql", "-U",
"postgres", "-c", query]).decode(self.get_stdout_encoding()).strip()
+
+ def check_query_results(self, query, number_of_rows, timeout_seconds):
+ start_time = time.perf_counter()
+ while (time.perf_counter() - start_time) < timeout_seconds:
+ if self.query_postgres_server(query, number_of_rows):
+ return True
+ time.sleep(2)
+ return False
+
def segfault_happened(self):
return self.segfault
diff --git a/docker/test/integration/minifi/core/SingleNodeDockerCluster.py
b/docker/test/integration/minifi/core/SingleNodeDockerCluster.py
index 3d30a90..07ecf65 100644
--- a/docker/test/integration/minifi/core/SingleNodeDockerCluster.py
+++ b/docker/test/integration/minifi/core/SingleNodeDockerCluster.py
@@ -114,6 +114,8 @@ class SingleNodeDockerCluster(Cluster):
self.deploy_s3_server()
elif self.engine == 'azure-storage-server':
self.deploy_azure_storage_server()
+ elif self.engine == 'postgresql-server':
+ self.deploy_postgres_server()
else:
raise Exception('invalid flow engine: \'%s\'' % self.engine)
@@ -122,6 +124,32 @@ class SingleNodeDockerCluster(Cluster):
# Build configured image
dockerfile = dedent("""FROM {base_image}
USER root
+ RUN apk --update --no-cache add psqlodbc
+ RUN echo "[PostgreSQL ANSI]" > /odbcinst.ini.template && \
+ echo "Description=PostgreSQL ODBC driver (ANSI version)"
>> /odbcinst.ini.template && \
+ echo "Driver=psqlodbca.so" >> /odbcinst.ini.template && \
+ echo "Setup=libodbcpsqlS.so" >> /odbcinst.ini.template && \
+ echo "Debug=0" >> /odbcinst.ini.template && \
+ echo "CommLog=1" >> /odbcinst.ini.template && \
+ echo "UsageCount=1" >> /odbcinst.ini.template && \
+ echo "" >> /odbcinst.ini.template && \
+ echo "[PostgreSQL Unicode]" >> /odbcinst.ini.template && \
+ echo "Description=PostgreSQL ODBC driver (Unicode
version)" >> /odbcinst.ini.template && \
+ echo "Driver=psqlodbcw.so" >> /odbcinst.ini.template && \
+ echo "Setup=libodbcpsqlS.so" >> /odbcinst.ini.template && \
+ echo "Debug=0" >> /odbcinst.ini.template && \
+ echo "CommLog=1" >> /odbcinst.ini.template && \
+ echo "UsageCount=1" >> /odbcinst.ini.template
+ RUN odbcinst -i -d -f /odbcinst.ini.template
+ RUN echo "[ODBC]" > /etc/odbc.ini && \
+ echo "Driver = PostgreSQL ANSI" >> /etc/odbc.ini && \
+ echo "Description = PostgreSQL Data Source" >>
/etc/odbc.ini && \
+ echo "Servername = postgres" >> /etc/odbc.ini && \
+ echo "Port = 5432" >> /etc/odbc.ini && \
+ echo "Protocol = 8.4" >> /etc/odbc.ini && \
+ echo "UserName = postgres" >> /etc/odbc.ini && \
+ echo "Password = password" >> /etc/odbc.ini && \
+ echo "Database = postgres" >> /etc/odbc.ini
ADD config.yml {minifi_root}/conf/config.yml
RUN chown minificpp:minificpp {minifi_root}/conf/config.yml
RUN sed -i -e 's/INFO/DEBUG/g'
{minifi_root}/conf/minifi-log.properties
@@ -283,6 +311,27 @@ class SingleNodeDockerCluster(Cluster):
ports={'10000/tcp': 10000, '10001/tcp': 10001})
self.containers[server.name] = server
+ def deploy_postgres_server(self):
+ dockerfile = dedent("""FROM {base_image}
+ RUN mkdir -p /docker-entrypoint-initdb.d
+ RUN echo "#!/bin/bash" >
/docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo "set -e" >>
/docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo "psql -v ON_ERROR_STOP=1 --username "postgres"
--dbname "postgres" <<-EOSQL" >> /docker-entrypoint-initdb.d/init-user-db.sh &&
\
+ echo " CREATE TABLE test_table (int_col INTEGER,
text_col TEXT);" >> /docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo " INSERT INTO test_table (int_col, text_col)
VALUES (1, 'apple');" >> /docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo " INSERT INTO test_table (int_col, text_col)
VALUES (2, 'banana');" >> /docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo " INSERT INTO test_table (int_col, text_col)
VALUES (3, 'pear');" >> /docker-entrypoint-initdb.d/init-user-db.sh && \
+ echo "EOSQL" >> /docker-entrypoint-initdb.d/init-user-db.sh
+ """.format(base_image='postgres:13.2'))
+ configured_image = self.build_image(dockerfile, [])
+ server = self.client.containers.run(
+ configured_image[0],
+ detach=True,
+ name='postgresql-server',
+ network=self.network.name,
+ environment=["POSTGRES_PASSWORD=password"])
+ self.containers[server.name] = server
+
def build_image(self, dockerfile, context_files):
conf_dockerfile_buffer = BytesIO()
docker_context_buffer = BytesIO()
diff --git a/docker/test/integration/minifi/processors/ExecuteSQL.py
b/docker/test/integration/minifi/processors/ExecuteSQL.py
new file mode 100644
index 0000000..9b05bb9
--- /dev/null
+++ b/docker/test/integration/minifi/processors/ExecuteSQL.py
@@ -0,0 +1,9 @@
+from ..core.Processor import Processor
+
+
+class ExecuteSQL(Processor):
+ def __init__(self, schedule={'scheduling strategy': 'EVENT_DRIVEN'}):
+ super(ExecuteSQL, self).__init__(
+ 'ExecuteSQL',
+ auto_terminate=['success'],
+ schedule=schedule)
diff --git a/docker/test/integration/minifi/processors/PutSQL.py
b/docker/test/integration/minifi/processors/PutSQL.py
new file mode 100644
index 0000000..b086f6e
--- /dev/null
+++ b/docker/test/integration/minifi/processors/PutSQL.py
@@ -0,0 +1,9 @@
+from ..core.Processor import Processor
+
+
+class PutSQL(Processor):
+ def __init__(self, schedule={'scheduling strategy': 'EVENT_DRIVEN'}):
+ super(PutSQL, self).__init__(
+ 'PutSQL',
+ auto_terminate=['success'],
+ schedule=schedule)
diff --git a/docker/test/integration/minifi/processors/QueryDatabaseTable.py
b/docker/test/integration/minifi/processors/QueryDatabaseTable.py
new file mode 100644
index 0000000..9312b60
--- /dev/null
+++ b/docker/test/integration/minifi/processors/QueryDatabaseTable.py
@@ -0,0 +1,8 @@
+from ..core.Processor import Processor
+
+
+class QueryDatabaseTable(Processor):
+ def __init__(self):
+ super(QueryDatabaseTable, self).__init__(
+ 'QueryDatabaseTable',
+ auto_terminate=['success'])
diff --git a/docker/test/integration/minifi/processors/UpdateAttribute.py
b/docker/test/integration/minifi/processors/UpdateAttribute.py
new file mode 100644
index 0000000..b40fc93
--- /dev/null
+++ b/docker/test/integration/minifi/processors/UpdateAttribute.py
@@ -0,0 +1,9 @@
+from ..core.Processor import Processor
+
+
+class UpdateAttribute(Processor):
+ def __init__(self, schedule={'scheduling strategy': 'EVENT_DRIVEN'}):
+ super(UpdateAttribute, self).__init__(
+ 'UpdateAttribute',
+ auto_terminate=['success'],
+ schedule=schedule)
diff --git a/docker/test/integration/steps/steps.py
b/docker/test/integration/steps/steps.py
index 0b4f835..0c45f7d 100644
--- a/docker/test/integration/steps/steps.py
+++ b/docker/test/integration/steps/steps.py
@@ -1,6 +1,5 @@
from minifi.core.FileSystemObserver import FileSystemObserver
from minifi.core.RemoteProcessGroup import RemoteProcessGroup
-from minifi.core.SSLContextService import SSLContextService
from minifi.core.SSL_cert_utils import gen_cert, rsa_gen_key_callback
from minifi.processors.ConsumeKafka import ConsumeKafka
@@ -10,6 +9,9 @@ from minifi.processors.PutAzureBlobStorage import
PutAzureBlobStorage
from minifi.processors.PublishKafka import PublishKafka
from minifi.processors.PutS3Object import PutS3Object
+from minifi.controllers.SSLContextService import SSLContextService
+from minifi.controllers.ODBCService import ODBCService
+
from behave import given, then, when
from behave.model_describe import ModelDescriptor
from pydoc import locate
@@ -346,6 +348,23 @@ def step_impl(context, topic_name):
print("Failed to create topic {}: {}".format(topic, e))
+# SQL
+@given("an ODBCService is setup up for {processor_name} with the name
\"{service_name}\" and connection string \"{connection_string}\"")
+def step_impl(context, processor_name, service_name, connection_string):
+ odbc_service = ODBCService(name=service_name,
connection_string=connection_string)
+ processor = context.test.get_node_by_name(processor_name)
+ processor.controller_services.append(odbc_service)
+ processor.set_property("DB Controller Service", odbc_service.name)
+
+
+@given("a PostgreSQL server \"{cluster_name}\" is set up")
+def step_impl(context, cluster_name):
+ cluster = context.test.acquire_cluster(cluster_name)
+ cluster.set_name(cluster_name)
+ cluster.set_engine("postgresql-server")
+ cluster.set_flow(None)
+
+
@when("the MiNiFi instance starts up")
@when("both instances start up")
@when("all instances start up")
@@ -450,11 +469,13 @@ def step_impl(context, cluster_name):
@then("a flowfile with the content \"{content}\" is placed in the monitored
directory in less than {duration}")
+@then("a flowfile with the content '{content}' is placed in the monitored
directory in less than {duration}")
def step_impl(context, content, duration):
context.test.check_for_single_file_with_content_generated(content,
timeparse(duration))
@then("at least one flowfile with the content \"{content}\" is placed in the
monitored directory in less than {duration}")
+@then("at least one flowfile with the content '{content}' is placed in the
monitored directory in less than {duration}")
def step_impl(context, content, duration):
context.test.check_for_at_least_one_file_with_content_generated(content,
timeparse(duration))
@@ -522,3 +543,9 @@ def step_impl(context, cluster_name):
@then("the object on the \"{cluster_name}\" Azure storage server is
\"{object_data}\"")
def step_impl(context, cluster_name, object_data):
context.test.check_azure_storage_server_data(cluster_name, object_data)
+
+
+# SQL
+@then("the query \"{query}\" returns {number_of_rows:d} rows in less than
{timeout_seconds:d} seconds on the \"{cluster_name}\" PostgreSQL server")
+def step_impl(context, cluster_name, query, number_of_rows, timeout_seconds):
+ context.test.check_query_results(cluster_name, query, number_of_rows,
timeout_seconds)
diff --git a/extensions/pdh/PDHCounters.cpp b/extensions/pdh/PDHCounters.cpp
index 3f52f65..1b880a0 100644
--- a/extensions/pdh/PDHCounters.cpp
+++ b/extensions/pdh/PDHCounters.cpp
@@ -30,7 +30,7 @@ DWORD PDHCounter::getDWFormat() const {
}
std::unique_ptr<PDHCounter> PDHCounter::createPDHCounter(const std::string&
query_name, bool is_double) {
- auto groups = utils::StringUtils::split(query_name, "\\");
+ auto groups = utils::StringUtils::splitRemovingEmpty(query_name, "\\");
if (groups.size() != 2 || query_name.substr(0, 1) != "\\")
return nullptr;
if (query_name.find("(*)") != std::string::npos) {
@@ -45,12 +45,12 @@ const std::string& PDHCounter::getName() const {
}
std::string PDHCounter::getObjectName() const {
- auto groups = utils::StringUtils::split(pdh_english_counter_name_, "\\");
+ auto groups =
utils::StringUtils::splitRemovingEmpty(pdh_english_counter_name_, "\\");
return groups[0];
}
std::string PDHCounter::getCounterName() const {
- auto groups = utils::StringUtils::split(pdh_english_counter_name_, "\\");
+ auto groups =
utils::StringUtils::splitRemovingEmpty(pdh_english_counter_name_, "\\");
return groups[1];
}
diff --git a/extensions/sql/data/DatabaseConnectors.h
b/extensions/sql/data/DatabaseConnectors.h
index e9e6437..40fec31 100644
--- a/extensions/sql/data/DatabaseConnectors.h
+++ b/extensions/sql/data/DatabaseConnectors.h
@@ -20,10 +20,8 @@
#include <memory>
#include <string>
-
-#include <soci/soci.h>
-
-#include "Utils.h"
+#include <vector>
+#include <ctime>
namespace org {
namespace apache {
@@ -31,61 +29,59 @@ namespace nifi {
namespace minifi {
namespace sql {
-/**
- * We do not intend to create an abstract facade here. We know that SOCI is
the underlying
- * SQL library. We only wish to abstract ODBC specific information
- */
+enum class DataType {
+ STRING,
+ DOUBLE,
+ INTEGER,
+ LONG_LONG,
+ UNSIGNED_LONG_LONG,
+ DATE
+};
-class Statement {
+class Row {
public:
+ virtual ~Row() = default;
+ virtual std::size_t size() const = 0;
+ virtual std::string getColumnName(std::size_t index) const = 0;
+ virtual bool isNull(std::size_t index) const = 0;
+ virtual DataType getDataType(std::size_t index) const = 0;
+ virtual std::string getString(std::size_t index) const = 0;
+ virtual double getDouble(std::size_t index) const = 0;
+ virtual int getInteger(std::size_t index) const = 0;
+ virtual long long getLongLong(std::size_t index) const = 0;
+ virtual unsigned long long getUnsignedLongLong(std::size_t index) const = 0;
+ virtual std::tm getDate(std::size_t index) const = 0;
+};
- explicit Statement(soci::session& session, const std::string &query)
- : session_(session), query_(query) {
+class Rowset {
+ public:
+ virtual ~Rowset() = default;
+ virtual void reset() = 0;
+ virtual bool is_done() = 0;
+ virtual Row& getCurrent() = 0;
+ virtual void next() = 0;
+};
+
+class Statement {
+ public:
+ explicit Statement(const std::string &query)
+ : query_(query) {
}
virtual ~Statement() = default;
-
- soci::rowset<soci::row> execute(const std::vector<std::string>& args = {}) {
- auto stmt = session_.prepare << query_;
- for (auto& arg : args) {
- // binds arguments to the prepared statement
- stmt.operator,(soci::use(arg));
- }
- return stmt;
- }
+ virtual std::unique_ptr<Rowset> execute(const std::vector<std::string> &args
= {}) = 0;
protected:
- soci::session& session_;
std::string query_;
};
class Session {
public:
-
- explicit Session(soci::session& session)
- : session_(session) {
- }
-
virtual ~Session() = default;
-
- void begin() {
- session_.begin();
- }
-
- void commit() {
- session_.commit();
- }
-
- void rollback() {
- session_.rollback();
- }
-
- void execute(const std::string &statement) {
- session_ << statement;
- }
-
-protected:
- soci::session& session_;
+ virtual void begin() = 0;
+ virtual void commit() = 0;
+ virtual void rollback() = 0;
+ virtual void execute(const std::string &statement) = 0;
};
class Connection {
diff --git a/extensions/sql/data/SQLRowsetProcessor.cpp
b/extensions/sql/data/SQLRowsetProcessor.cpp
index 136385d..3d3b5ad 100644
--- a/extensions/sql/data/SQLRowsetProcessor.cpp
+++ b/extensions/sql/data/SQLRowsetProcessor.cpp
@@ -28,9 +28,9 @@ namespace nifi {
namespace minifi {
namespace sql {
-SQLRowsetProcessor::SQLRowsetProcessor(const soci::rowset<soci::row>& rowset,
std::vector<std::reference_wrapper<SQLRowSubscriber>> row_subscribers)
- : rowset_(rowset), row_subscribers_(std::move(row_subscribers)) {
- iter_ = rowset_.begin();
+SQLRowsetProcessor::SQLRowsetProcessor(std::unique_ptr<Rowset> rowset,
std::vector<std::reference_wrapper<SQLRowSubscriber>> row_subscribers)
+ : rowset_(std::move(rowset)), row_subscribers_(std::move(row_subscribers)) {
+ rowset_->reset();
}
size_t SQLRowsetProcessor::process(size_t max) {
@@ -40,9 +40,9 @@ size_t SQLRowsetProcessor::process(size_t max) {
subscriber.get().beginProcessBatch();
}
- for (; iter_ != rowset_.end(); ) {
- addRow(*iter_, count);
- iter_++;
+ while (!rowset_->is_done()) {
+ addRow(rowset_->getCurrent(), count);
+ rowset_->next();
count++;
if (max > 0 && count >= max) {
break;
@@ -59,7 +59,7 @@ size_t SQLRowsetProcessor::process(size_t max) {
return count;
}
-void SQLRowsetProcessor::addRow(const soci::row& row, size_t rowCount) {
+void SQLRowsetProcessor::addRow(const Row& row, size_t rowCount) {
for (const auto& subscriber : row_subscribers_) {
subscriber.get().beginProcessRow();
}
@@ -68,7 +68,7 @@ void SQLRowsetProcessor::addRow(const soci::row& row, size_t
rowCount) {
std::vector<std::string> column_names;
column_names.reserve(row.size());
for (std::size_t i = 0; i != row.size(); ++i) {
-
column_names.push_back(utils::StringUtils::toLower(row.get_properties(i).get_name()));
+
column_names.push_back(utils::StringUtils::toLower(row.getColumnName(i)));
}
for (const auto& subscriber : row_subscribers_) {
subscriber.get().processColumnNames(column_names);
@@ -76,36 +76,34 @@ void SQLRowsetProcessor::addRow(const soci::row& row,
size_t rowCount) {
}
for (std::size_t i = 0; i != row.size(); ++i) {
- const soci::column_properties& props = row.get_properties(i);
+ const auto& name = utils::StringUtils::toLower(row.getColumnName(i));
- const auto& name = utils::StringUtils::toLower(props.get_name());
-
- if (row.get_indicator(i) == soci::i_null) {
+ if (row.isNull(i)) {
processColumn(name, "NULL");
} else {
- switch (const auto dataType = props.get_data_type()) {
- case soci::data_type::dt_string: {
- processColumn(name, row.get<std::string>(i));
+ switch (row.getDataType(i)) {
+ case DataType::STRING: {
+ processColumn(name, row.getString(i));
}
break;
- case soci::data_type::dt_double: {
- processColumn(name, row.get<double>(i));
+ case DataType::DOUBLE: {
+ processColumn(name, row.getDouble(i));
}
break;
- case soci::data_type::dt_integer: {
- processColumn(name, row.get<int>(i));
+ case DataType::INTEGER: {
+ processColumn(name, row.getInteger(i));
}
break;
- case soci::data_type::dt_long_long: {
- processColumn(name, row.get<long long>(i));
+ case DataType::LONG_LONG: {
+ processColumn(name, row.getLongLong(i));
}
break;
- case soci::data_type::dt_unsigned_long_long: {
- processColumn(name, row.get<unsigned long long>(i));
+ case DataType::UNSIGNED_LONG_LONG: {
+ processColumn(name, row.getUnsignedLongLong(i));
}
break;
- case soci::data_type::dt_date: {
- const std::tm when = row.get<std::tm>(i);
+ case DataType::DATE: {
+ const std::tm when = row.getDate(i);
char value[128];
if (!std::strftime(value, sizeof(value), "%Y-%m-%d %H:%M:%S", &when))
@@ -115,7 +113,7 @@ void SQLRowsetProcessor::addRow(const soci::row& row,
size_t rowCount) {
}
break;
default: {
- throw minifi::Exception(PROCESSOR_EXCEPTION, "SQLRowsetProcessor:
Unsupported data type " + std::to_string(dataType));
+ throw minifi::Exception(PROCESSOR_EXCEPTION, "SQLRowsetProcessor:
Unsupported data type in column");
}
}
}
diff --git a/extensions/sql/data/SQLRowsetProcessor.h
b/extensions/sql/data/SQLRowsetProcessor.h
index 3c5abc3..5f52c42 100644
--- a/extensions/sql/data/SQLRowsetProcessor.h
+++ b/extensions/sql/data/SQLRowsetProcessor.h
@@ -20,9 +20,8 @@
#include <vector>
-#include <soci/soci.h>
-
#include "SQLRowSubscriber.h"
+#include "DatabaseConnectors.h"
namespace org {
namespace apache {
@@ -32,12 +31,12 @@ namespace sql {
class SQLRowsetProcessor {
public:
- SQLRowsetProcessor(const soci::rowset<soci::row>& rowset,
std::vector<std::reference_wrapper<SQLRowSubscriber>> rowSubscribers);
+ SQLRowsetProcessor(std::unique_ptr<Rowset> rowset,
std::vector<std::reference_wrapper<SQLRowSubscriber>> rowSubscribers);
size_t process(size_t max);
private:
- void addRow(const soci::row& row, size_t rowCount);
+ void addRow(const Row& row, size_t rowCount);
template <typename T>
void processColumn(const std::string& name, const T& value) const {
@@ -47,8 +46,7 @@ class SQLRowsetProcessor {
}
private:
- soci::rowset<soci::row>::const_iterator iter_;
- soci::rowset<soci::row> rowset_;
+ std::unique_ptr<Rowset> rowset_;
std::vector<std::reference_wrapper<SQLRowSubscriber>> row_subscribers_;
};
diff --git a/extensions/sql/data/SociConnectors.cpp
b/extensions/sql/data/SociConnectors.cpp
new file mode 100644
index 0000000..235c481
--- /dev/null
+++ b/extensions/sql/data/SociConnectors.cpp
@@ -0,0 +1,181 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+#include "SociConnectors.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace sql {
+
+void SociRow::setIterator(const soci::rowset<soci::row>::iterator& iter) {
+ current_ = iter;
+}
+
+soci::rowset<soci::row>::iterator SociRow::getIterator() const {
+ return current_;
+}
+
+void SociRow::next() {
+ ++current_;
+}
+
+std::size_t SociRow::size() const {
+ return current_->size();
+}
+
+std::string SociRow::getColumnName(std::size_t index) const {
+ return current_->get_properties(index).get_name();
+}
+
+bool SociRow::isNull(std::size_t index) const {
+ return current_->get_indicator(index) == soci::i_null;
+}
+
+DataType SociRow::getDataType(std::size_t index) const {
+ switch (const auto dataType =
current_->get_properties(index).get_data_type()) {
+ case soci::data_type::dt_string: {
+ return DataType::STRING;
+ }
+ case soci::data_type::dt_double: {
+ return DataType::DOUBLE;
+ }
+ case soci::data_type::dt_integer: {
+ return DataType::INTEGER;
+ }
+ case soci::data_type::dt_long_long: {
+ return DataType::LONG_LONG;
+ }
+ case soci::data_type::dt_unsigned_long_long: {
+ return DataType::UNSIGNED_LONG_LONG;
+ }
+ case soci::data_type::dt_date: {
+ return DataType::DATE;
+ }
+ default: {
+ throw minifi::Exception(PROCESSOR_EXCEPTION, "SQLRowsetProcessor:
Unsupported data type " + std::to_string(dataType));
+ }
+ }
+}
+
+std::string SociRow::getString(std::size_t index) const {
+ return current_->get<std::string>(index);
+}
+
+double SociRow::getDouble(std::size_t index) const {
+ return current_->get<double>(index);
+}
+
+int SociRow::getInteger(std::size_t index) const {
+ return current_->get<int>(index);
+}
+
+long long SociRow::getLongLong(std::size_t index) const {
+ return current_->get<long long>(index);
+}
+
+unsigned long long SociRow::getUnsignedLongLong(std::size_t index) const {
+ return current_->get<unsigned long long>(index);
+}
+
+std::tm SociRow::getDate(std::size_t index) const {
+ return current_->get<std::tm>(index);
+}
+
+void SociRowset::reset() {
+ current_row_.setIterator(rowset_.begin());
+}
+
+bool SociRowset::is_done() {
+ return current_row_.getIterator() == rowset_.end();
+}
+
+Row& SociRowset::getCurrent() {
+ return current_row_;
+}
+
+void SociRowset::next() {
+ current_row_.next();
+}
+
+std::unique_ptr<Rowset> SociStatement::execute(const std::vector<std::string>&
args) {
+ auto stmt = session_.prepare << query_;
+ for (auto& arg : args) {
+ // binds arguments to the prepared statement
+ stmt.operator,(soci::use(arg));
+ }
+ return utils::make_unique<SociRowset>(stmt);
+}
+
+void SociSession::begin() {
+ session_.begin();
+}
+
+void SociSession::commit() {
+ session_.commit();
+}
+
+void SociSession::rollback() {
+ session_.rollback();
+}
+
+void SociSession::execute(const std::string &statement) {
+ session_ << statement;
+}
+
+ODBCConnection::ODBCConnection(std::string connectionString)
+ : connection_string_(std::move(connectionString)) {
+ session_ = utils::make_unique<soci::session>(getSessionParameters());
+}
+
+bool ODBCConnection::connected(std::string& exception) const {
+ try {
+ exception.clear();
+ // According to
https://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most
by Rob Hruska,
+ // 'select 1' works for: H2, MySQL, Microsoft SQL Server, PostgreSQL,
SQLite. For Oracle 'SELECT 1 FROM DUAL' works.
+ prepareStatement("select 1")->execute();
+ return true;
+ } catch (const std::exception& e) {
+ exception = e.what();
+ return false;
+ }
+}
+
+std::unique_ptr<sql::Statement> ODBCConnection::prepareStatement(const
std::string& query) const {
+ return utils::make_unique<sql::SociStatement>(*session_, query);
+}
+
+std::unique_ptr<Session> ODBCConnection::getSession() const {
+ return utils::make_unique<sql::SociSession>(*session_);
+}
+
+soci::connection_parameters ODBCConnection::getSessionParameters() const {
+ static const soci::backend_factory &backEnd = *soci::factory_odbc();
+
+ soci::connection_parameters parameters(backEnd, connection_string_);
+ parameters.set_option(soci::odbc_option_driver_complete, "0" /*
SQL_DRIVER_NOPROMPT */);
+
+ return parameters;
+}
+
+} /* namespace sql */
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/extensions/sql/data/SociConnectors.h
b/extensions/sql/data/SociConnectors.h
new file mode 100644
index 0000000..388adf8
--- /dev/null
+++ b/extensions/sql/data/SociConnectors.h
@@ -0,0 +1,120 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <ctime>
+
+#include "Exception.h"
+#include "utils/GeneralUtils.h"
+#include "data/DatabaseConnectors.h"
+#include <soci/soci.h>
+#include <soci/odbc/soci-odbc.h>
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace sql {
+
+class SociRow : public Row {
+ public:
+ void setIterator(const soci::rowset<soci::row>::iterator& iter);
+ soci::rowset<soci::row>::iterator getIterator() const;
+ void next();
+
+ std::size_t size() const override;
+ std::string getColumnName(std::size_t index) const override;
+ bool isNull(std::size_t index) const override;
+ DataType getDataType(std::size_t index) const override;
+ std::string getString(std::size_t index) const override;
+ double getDouble(std::size_t index) const override;
+ int getInteger(std::size_t index) const override;
+ long long getLongLong(std::size_t index) const override;
+ unsigned long long getUnsignedLongLong(std::size_t index) const override;
+ std::tm getDate(std::size_t index) const override;
+
+ private:
+ soci::rowset<soci::row>::iterator current_;
+};
+
+class SociRowset : public Rowset {
+ public:
+ SociRowset(const soci::rowset<soci::row>& rowset) : rowset_(rowset) {
+ }
+
+ void reset() override;
+ bool is_done() override;
+ Row& getCurrent() override;
+ void next() override;
+
+ private:
+ soci::rowset<soci::row> rowset_;
+ SociRow current_row_;
+};
+
+class SociStatement : public Statement {
+ public:
+ explicit SociStatement(soci::session& session, const std::string &query)
+ : Statement(query), session_(session) {
+ }
+
+ std::unique_ptr<Rowset> execute(const std::vector<std::string>& args = {})
override;
+
+ protected:
+ soci::session& session_;
+};
+
+class SociSession : public Session {
+ public:
+ explicit SociSession(soci::session& session)
+ : session_(session) {
+ }
+
+ void begin() override;
+ void commit() override;
+ void rollback() override;
+ void execute(const std::string &statement) override;
+
+protected:
+ soci::session& session_;
+};
+
+class ODBCConnection : public sql::Connection {
+ public:
+ explicit ODBCConnection(std::string connectionString);
+
+ bool connected(std::string& exception) const override;
+ std::unique_ptr<sql::Statement> prepareStatement(const std::string& query)
const override;
+ std::unique_ptr<Session> getSession() const override;
+
+ private:
+ soci::connection_parameters getSessionParameters() const;
+
+ private:
+ std::unique_ptr<soci::session> session_;
+ std::string connection_string_;
+};
+
+} /* namespace sql */
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/extensions/sql/data/Utils.cpp b/extensions/sql/data/Utils.cpp
index 069ec0b..33273e2 100644
--- a/extensions/sql/data/Utils.cpp
+++ b/extensions/sql/data/Utils.cpp
@@ -30,11 +30,10 @@ namespace minifi {
namespace utils {
std::vector<std::string> inputStringToList(const std::string& str) {
- std::vector<std::string> fragments = StringUtils::split(str, ",");
+ std::vector<std::string> fragments =
StringUtils::splitAndTrimRemovingEmpty(str, ",");
for (auto& item : fragments) {
- item = StringUtils::toLower(StringUtils::trim(item));
+ item = StringUtils::toLower(item);
}
- fragments.erase(std::remove(fragments.begin(), fragments.end(), ""),
fragments.end());
return fragments;
}
diff --git a/extensions/sql/processors/ExecuteSQL.cpp
b/extensions/sql/processors/ExecuteSQL.cpp
index b952fb7..f8e8acb 100644
--- a/extensions/sql/processors/ExecuteSQL.cpp
+++ b/extensions/sql/processors/ExecuteSQL.cpp
@@ -100,7 +100,7 @@ void ExecuteSQL::processOnTrigger(core::ProcessContext&
context, core::ProcessSe
sql::JSONSQLWriter json_writer{output_format_ == OutputType::JSONPretty};
FlowFileGenerator flow_file_creator{session, json_writer};
- sql::SQLRowsetProcessor sql_rowset_processor(row_set, {json_writer,
flow_file_creator});
+ sql::SQLRowsetProcessor sql_rowset_processor(std::move(row_set),
{json_writer, flow_file_creator});
// Process rowset.
while (size_t row_count = sql_rowset_processor.process(max_rows_)) {
diff --git a/extensions/sql/processors/QueryDatabaseTable.cpp
b/extensions/sql/processors/QueryDatabaseTable.cpp
index e49bdb5..4cc4efa 100644
--- a/extensions/sql/processors/QueryDatabaseTable.cpp
+++ b/extensions/sql/processors/QueryDatabaseTable.cpp
@@ -157,7 +157,7 @@ void
QueryDatabaseTable::processOnTrigger(core::ProcessContext& /*context*/, cor
};
sql::JSONSQLWriter json_writer{output_format_ == OutputType::JSONPretty,
column_filter};
FlowFileGenerator flow_file_creator{session, json_writer};
- sql::SQLRowsetProcessor sql_rowset_processor(rowset, {json_writer,
maxCollector, flow_file_creator});
+ sql::SQLRowsetProcessor sql_rowset_processor(std::move(rowset),
{json_writer, maxCollector, flow_file_creator});
while (size_t row_count = sql_rowset_processor.process(max_rows_)) {
auto new_file = flow_file_creator.getLastFlowFile();
diff --git a/extensions/sql/services/DatabaseService.h
b/extensions/sql/services/DatabaseService.h
index cf34fd4..4d3decc 100644
--- a/extensions/sql/services/DatabaseService.h
+++ b/extensions/sql/services/DatabaseService.h
@@ -15,16 +15,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-#ifndef LIBMINIFI_INCLUDE_CONTROLLERS_DATABASESERVICE_H_
-#define LIBMINIFI_INCLUDE_CONTROLLERS_DATABASESERVICE_H_
+#pragma once
-#include "core/logging/LoggerConfiguration.h"
-#include "core/controller/ControllerService.h"
-#include "data/DatabaseConnectors.h"
#include <memory>
#include <unordered_map>
-#include <soci/soci.h>
+#include "core/logging/LoggerConfiguration.h"
+#include "core/controller/ControllerService.h"
+#include "data/DatabaseConnectors.h"
namespace org {
namespace apache {
@@ -99,5 +97,3 @@ class DatabaseService : public
core::controller::ControllerService {
} /* namespace nifi */
} /* namespace apache */
} /* namespace org */
-
-#endif /* LIBMINIFI_INCLUDE_CONTROLLERS_DATABASESERVICE_H_ */
diff --git a/extensions/sql/services/ODBCConnector.h
b/extensions/sql/services/ODBCConnector.h
index 16a3562..692ce12 100644
--- a/extensions/sql/services/ODBCConnector.h
+++ b/extensions/sql/services/ODBCConnector.h
@@ -20,18 +20,9 @@
#include "core/logging/LoggerConfiguration.h"
#include "core/controller/ControllerService.h"
-
-#include "utils/GeneralUtils.h"
#include "DatabaseService.h"
#include "core/Resource.h"
-#include "data/DatabaseConnectors.h"
-#include <memory>
-#include <unordered_map>
-
-#include <soci/soci.h>
-#include <soci/odbc/soci-odbc.h>
-
-#include <iostream>
+#include "data/SociConnectors.h"
namespace org {
namespace apache {
@@ -40,49 +31,6 @@ namespace minifi {
namespace sql {
namespace controllers {
-class ODBCConnection : public sql::Connection {
- public:
- explicit ODBCConnection(std::string connectionString)
- : connection_string_(std::move(connectionString)) {
- session_ = utils::make_unique<soci::session>(getSessionParameters());
- }
-
- bool connected(std::string& exception) const override {
- try {
- exception.clear();
- // According to
https://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most
by Rob Hruska,
- // 'select 1' works for: H2, MySQL, Microsoft SQL Server, PostgreSQL,
SQLite. For Oracle 'SELECT 1 FROM DUAL' works.
- prepareStatement("select 1")->execute();
- return true;
- } catch (const std::exception& e) {
- exception = e.what();
- return false;
- }
- }
-
- std::unique_ptr<sql::Statement> prepareStatement(const std::string& query)
const override {
- return utils::make_unique<sql::Statement>(*session_, query);
- }
-
- std::unique_ptr<Session> getSession() const override {
- return utils::make_unique<sql::Session>(*session_);
- }
-
- private:
- soci::connection_parameters getSessionParameters() const {
- static const soci::backend_factory &backEnd = *soci::factory_odbc();
-
- soci::connection_parameters parameters(backEnd, connection_string_);
- parameters.set_option(soci::odbc_option_driver_complete, "0" /*
SQL_DRIVER_NOPROMPT */);
-
- return parameters;
- }
-
- private:
- std::unique_ptr<soci::session> session_;
- std::string connection_string_;
-};
-
/**
* Purpose and Justification: Controller services function as a layerable way
to provide
* services to internal services. While a controller service is generally
configured from the flow,
diff --git a/libminifi/include/utils/StringUtils.h
b/libminifi/include/utils/StringUtils.h
index 6dbeea6..de6b6cb 100644
--- a/libminifi/include/utils/StringUtils.h
+++ b/libminifi/include/utils/StringUtils.h
@@ -136,7 +136,9 @@ class StringUtils {
}
static std::vector<std::string> split(const std::string &str, const
std::string &delimiter);
+ static std::vector<std::string> splitRemovingEmpty(const std::string& str,
const std::string& delimiter);
static std::vector<std::string> splitAndTrim(const std::string &str, const
std::string &delimiter);
+ static std::vector<std::string> splitAndTrimRemovingEmpty(const std::string&
str, const std::string& delimiter);
/**
* Converts a string to a float
diff --git a/libminifi/src/utils/StringUtils.cpp
b/libminifi/src/utils/StringUtils.cpp
index 8028560..13667cb 100644
--- a/libminifi/src/utils/StringUtils.cpp
+++ b/libminifi/src/utils/StringUtils.cpp
@@ -48,27 +48,26 @@ std::string StringUtils::trim(const std::string& s) {
}
template<typename Fun>
-std::vector<std::string> split_transformed(const std::string& str, const
std::string& delimiter, Fun transformation) {
+std::vector<std::string> split_transformed(std::string str, const std::string&
delimiter, Fun transformation) {
std::vector<std::string> result;
if (delimiter.empty()) {
- std::transform(str.begin(), str.end(), std::back_inserter(result), [&]
(const char c) { return transformation(std::string{c}); });
- return result;
- }
- auto curr = str.begin();
- auto end = str.end();
- auto is_func = [delimiter](int s) {
- return delimiter.at(0) == s;
- };
- while (curr != end) {
- curr = std::find_if_not(curr, end, is_func);
- if (curr == end) {
- break;
+ for (auto c : str) {
+ result.push_back(transformation(std::string(1, c)));
}
- auto next = std::find_if(curr, end, is_func);
- result.push_back(transformation(std::string(curr, next)));
- curr = next;
+ return result;
}
+ size_t pos = str.find(delimiter);
+ if (pos == std::string::npos) {
+ result.push_back(transformation(str));
+ return result;
+ }
+ while (pos != std::string::npos) {
+ result.push_back(transformation(str.substr(0, pos)));
+ str = str.substr(pos + delimiter.size());
+ pos = str.find(delimiter);
+ }
+ result.push_back(transformation(str));
return result;
}
@@ -76,10 +75,22 @@ std::vector<std::string> StringUtils::split(const
std::string& str, const std::s
return split_transformed(str, delimiter, identity{});
}
+std::vector<std::string> StringUtils::splitRemovingEmpty(const std::string&
str, const std::string& delimiter) {
+ auto result = split(str, delimiter);
+ result.erase(std::remove_if(result.begin(), result.end(), [](const
std::string& str) { return str.empty(); }), result.end());
+ return result;
+}
+
std::vector<std::string> StringUtils::splitAndTrim(const std::string& str,
const std::string& delimiter) {
return split_transformed(str, delimiter, trim);
}
+std::vector<std::string> StringUtils::splitAndTrimRemovingEmpty(const
std::string& str, const std::string& delimiter) {
+ auto result = splitAndTrim(str, delimiter);
+ result.erase(std::remove_if(result.begin(), result.end(), [](const
std::string& str) { return str.empty(); }), result.end());
+ return result;
+}
+
bool StringUtils::StringToFloat(std::string input, float &output,
FailurePolicy cp /*= RETURN*/) {
try {
output = std::stof(input);
diff --git a/libminifi/src/utils/tls/DistinguishedName.cpp
b/libminifi/src/utils/tls/DistinguishedName.cpp
index b506b91..229f13b 100644
--- a/libminifi/src/utils/tls/DistinguishedName.cpp
+++ b/libminifi/src/utils/tls/DistinguishedName.cpp
@@ -35,11 +35,11 @@ DistinguishedName::DistinguishedName(const
std::vector<std::string>& components)
}
DistinguishedName DistinguishedName::fromCommaSeparated(const std::string&
comma_separated_components) {
- return
DistinguishedName{utils::StringUtils::split(comma_separated_components, ",")};
+ return
DistinguishedName{utils::StringUtils::splitRemovingEmpty(comma_separated_components,
",")};
}
DistinguishedName DistinguishedName::fromSlashSeparated(const std::string
&slash_separated_components) {
- return
DistinguishedName{utils::StringUtils::split(slash_separated_components, "/")};
+ return
DistinguishedName{utils::StringUtils::splitRemovingEmpty(slash_separated_components,
"/")};
}
utils::optional<std::string> DistinguishedName::getCN() const {
diff --git a/libminifi/test/sql-tests/CMakeLists.txt
b/libminifi/test/sql-tests/CMakeLists.txt
index ae3d1ee..9837871 100644
--- a/libminifi/test/sql-tests/CMakeLists.txt
+++ b/libminifi/test/sql-tests/CMakeLists.txt
@@ -16,8 +16,14 @@
# specific language governing permissions and limitations
# under the License.
-file(GLOB SQL_TESTS "*.cpp")
+file(GLOB SQL_MOCK_SOURCES "mocks/*.cpp")
+add_library(minifi-sql-mocks STATIC ${SQL_MOCK_SOURCES})
+target_include_directories(minifi-sql-mocks BEFORE PRIVATE
"${CMAKE_SOURCE_DIR}/libminifi/include")
+target_include_directories(minifi-sql-mocks BEFORE PRIVATE
"${CMAKE_SOURCE_DIR}/extensions/sql")
+target_include_directories(minifi-sql-mocks BEFORE PRIVATE "mocks")
+target_wholearchive_library(minifi-sql-mocks core-minifi)
+file(GLOB SQL_TESTS "*.cpp")
set(SQL_TEST_COUNT 0)
foreach(testfile ${SQL_TESTS})
get_filename_component(testfilename "${testfile}" NAME_WE)
@@ -31,6 +37,7 @@ foreach(testfile ${SQL_TESTS})
createTests("${testfilename}")
target_link_libraries(${testfilename} ${CATCH_MAIN_LIB})
+ target_link_libraries(${testfilename} minifi-sql-mocks)
target_wholearchive_library(${testfilename} minifi-sql)
target_wholearchive_library(${testfilename} minifi-standard-processors)
target_wholearchive_library(${testfilename}
minifi-expression-language-extensions)
diff --git a/libminifi/test/sql-tests/PutSQLTests.cpp
b/libminifi/test/sql-tests/PutSQLTests.cpp
index e5e19cb..5f3b557 100644
--- a/libminifi/test/sql-tests/PutSQLTests.cpp
+++ b/libminifi/test/sql-tests/PutSQLTests.cpp
@@ -42,12 +42,11 @@ TEST_CASE("Test Put", "[PutSQLPut]") {
auto input_file = plan->addInput({
{"sql.args.1.value", "42"},
- {"sql.args.2.value", "asdf"}
});
sql_proc->setProperty(
"SQL Statement",
- "INSERT INTO test_table (int_col, text_col) VALUES (?, ?)");
+ "INSERT INTO test_table (int_col, text_col) VALUES (?, 'asdf')");
plan->run();
diff --git a/libminifi/test/sql-tests/SQLTestController.h
b/libminifi/test/sql-tests/SQLTestController.h
index 833d5c6..51ad6ae 100644
--- a/libminifi/test/sql-tests/SQLTestController.h
+++ b/libminifi/test/sql-tests/SQLTestController.h
@@ -33,7 +33,13 @@
#include "processors/QueryDatabaseTable.h"
#include "SQLTestPlan.h"
+#ifdef USE_REAL_ODBC_TEST_DRIVER
#include "services/ODBCConnector.h"
+using ODBCConnection = minifi::sql::ODBCConnection;
+#else
+#include "mocks/MockConnectors.h"
+using ODBCConnection = minifi::sql::MockODBCConnection;
+#endif
struct TableRow {
int64_t int_col;
@@ -63,8 +69,8 @@ class SQLTestController : public TestController {
connection_str_ = "Driver=" + DRIVER + ";Database=" + database_.str();
// Create test dbs
-
minifi::sql::controllers::ODBCConnection{connection_str_}.prepareStatement("CREATE
TABLE test_table (int_col INTEGER, text_col TEXT);")->execute();
-
minifi::sql::controllers::ODBCConnection{connection_str_}.prepareStatement("CREATE
TABLE empty_test_table (int_col INTEGER, text_col TEXT);")->execute();
+ ODBCConnection{connection_str_}.prepareStatement("CREATE TABLE test_table
(int_col INTEGER, text_col TEXT);")->execute();
+ ODBCConnection{connection_str_}.prepareStatement("CREATE TABLE
empty_test_table (int_col INTEGER, text_col TEXT);")->execute();
}
std::shared_ptr<SQLTestPlan> createSQLPlan(const std::string& sql_processor,
std::initializer_list<core::Relationship> outputs) {
@@ -72,7 +78,7 @@ class SQLTestController : public TestController {
}
void insertValues(std::initializer_list<TableRow> values) {
- minifi::sql::controllers::ODBCConnection connection{connection_str_};
+ ODBCConnection connection{connection_str_};
for (const auto& value : values) {
connection.prepareStatement("INSERT INTO test_table (int_col, text_col)
VALUES (?, ?);")
->execute({std::to_string(value.int_col), value.text_col});
@@ -81,10 +87,11 @@ class SQLTestController : public TestController {
std::vector<TableRow> fetchValues() {
std::vector<TableRow> rows;
- minifi::sql::controllers::ODBCConnection connection{connection_str_};
- auto soci_rowset = connection.prepareStatement("SELECT * FROM
test_table;")->execute();
- for (const auto& soci_row : soci_rowset) {
- rows.push_back(TableRow{get_column_cast<int64_t>(soci_row, "int_col"),
soci_row.get<std::string>("text_col")});
+ ODBCConnection connection{connection_str_};
+ auto rowset = connection.prepareStatement("SELECT * FROM
test_table;")->execute();
+ for (rowset->reset(); !rowset->is_done(); rowset->next()) {
+ const auto& row = rowset->getCurrent();
+ rows.push_back(TableRow{row.getInteger(0), row.getString(1)});
}
return rows;
}
@@ -94,21 +101,6 @@ class SQLTestController : public TestController {
}
private:
- template<typename T>
- T get_column_cast(const soci::row& row, const std::string& column_name) {
- const auto& column_props = row.get_properties(column_name);
- switch (const auto data_type = column_props.get_data_type()) {
- case soci::data_type::dt_integer:
- return gsl::narrow<T>(row.get<int>(column_name));
- case soci::data_type::dt_long_long:
- return gsl::narrow<T>(row.get<long long>(column_name)); // NOLINT
- case soci::data_type::dt_unsigned_long_long:
- return gsl::narrow<T>(row.get<unsigned long long>(column_name)); //
NOLINT
- default:
- throw std::logic_error("Unknown data type for column \"" + column_name
+ "\"");
- }
- }
-
utils::Path test_dir_;
utils::Path database_;
std::string connection_str_;
diff --git a/libminifi/test/sql-tests/SQLTestPlan.h
b/libminifi/test/sql-tests/SQLTestPlan.h
index c69d847..1e348c9 100644
--- a/libminifi/test/sql-tests/SQLTestPlan.h
+++ b/libminifi/test/sql-tests/SQLTestPlan.h
@@ -27,6 +27,13 @@
#include "../TestBase.h"
+#ifdef USE_REAL_ODBC_TEST_DRIVER
+static const std::string ODBC_SERVICE = "ODBCService";
+#else
+#include "mocks/MockODBCService.h"
+static const std::string ODBC_SERVICE = "MockODBCService";
+#endif
+
class SQLTestPlan {
public:
SQLTestPlan(TestController& controller, const std::string& connection_str,
const std::string& sql_processor, std::initializer_list<core::Relationship>
output_rels) {
@@ -39,7 +46,7 @@ class SQLTestPlan {
}
// initialize database service
- auto service = plan_->addController("ODBCService", "ODBCService");
+ auto service = plan_->addController(ODBC_SERVICE, "ODBCService");
plan_->setProperty(service,
minifi::sql::controllers::DatabaseService::ConnectionString.getName(),
connection_str);
}
diff --git a/libminifi/test/sql-tests/mocks/MockConnectors.cpp
b/libminifi/test/sql-tests/mocks/MockConnectors.cpp
new file mode 100644
index 0000000..f963867
--- /dev/null
+++ b/libminifi/test/sql-tests/mocks/MockConnectors.cpp
@@ -0,0 +1,416 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+#include "MockConnectors.h"
+
+#include <fstream>
+#include <algorithm>
+#include <utility>
+#include <string>
+#include <memory>
+
+#include "utils/GeneralUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace sql {
+
+std::size_t MockRow::size() const {
+ return column_names_->size();
+}
+
+std::string MockRow::getColumnName(std::size_t index) const {
+ return column_names_->at(index);
+}
+
+bool MockRow::isNull(std::size_t index) const {
+ return column_values_.at(index) == "NULL";
+}
+
+DataType MockRow::getDataType(std::size_t index) const {
+ return column_types_->at(index);
+}
+
+std::string MockRow::getString(std::size_t index) const {
+ return column_values_.at(index);
+}
+
+double MockRow::getDouble(std::size_t index) const {
+ return std::stof(column_values_.at(index));
+}
+
+int MockRow::getInteger(std::size_t index) const {
+ return std::stoi(column_values_.at(index));
+}
+
+long long MockRow::getLongLong(std::size_t index) const { // NOLINT
+ return std::stoll(column_values_.at(index));
+}
+
+unsigned long long MockRow::getUnsignedLongLong(std::size_t index) const { //
NOLINT
+ return static_cast<unsigned long
long>(std::stoll(column_values_.at(index))); // NOLINT
+}
+
+std::tm MockRow::getDate(std::size_t /*index*/) const {
+ throw std::runtime_error("date not implemented");
+}
+
+std::vector<std::string> MockRow::getValues() const {
+ return column_values_;
+}
+
+std::string MockRow::getValue(const std::string& col_name) const {
+ auto it = std::find(column_names_->begin(), column_names_->end(), col_name);
+ if (it != column_names_->end()) {
+ return column_values_.at(it-column_names_->begin());
+ }
+ throw std::runtime_error("Unknown column name for getting value");
+}
+
+DataType MockRow::getDataType(const std::string& col_name) const {
+ auto it = std::find(column_names_->begin(), column_names_->end(), col_name);
+ if (it != column_names_->end()) {
+ return column_types_->at(it-column_names_->begin());
+ }
+ throw std::runtime_error("Unknown column name for getting type");
+}
+
+void MockRowset::addRow(const std::vector<std::string>& column_values) {
+ rows_.emplace_back(&column_names_, &column_types_, column_values);
+}
+
+void MockRowset::reset() {
+ current_row_ = rows_.begin();
+}
+
+bool MockRowset::is_done() {
+ return current_row_ == rows_.end();
+}
+
+Row& MockRowset::getCurrent() {
+ return *current_row_;
+}
+
+void MockRowset::next() {
+ ++current_row_;
+}
+
+std::vector<std::string> MockRowset::getColumnNames() const {
+ return column_names_;
+}
+
+std::vector<DataType> MockRowset::getColumnTypes() const {
+ return column_types_;
+}
+
+std::vector<MockRow> MockRowset::getRows() const {
+ return rows_;
+}
+
+std::size_t MockRowset::getColumnIndex(const std::string& col_name) const {
+ auto it = std::find(column_names_.begin(), column_names_.end(), col_name);
+ if (it != column_names_.end()) {
+ return it-column_names_.begin();
+ }
+ throw std::runtime_error("Unknown column name for getting index");
+}
+
+void MockRowset::sort(const std::string& order_by_col, bool order_ascending) {
+ std::sort(rows_.begin(), rows_.end(), [&](const MockRow& first, const
MockRow& second) {
+ if (order_ascending) {
+ return first.getValue(order_by_col) < second.getValue(order_by_col);
+ } else {
+ return first.getValue(order_by_col) > second.getValue(order_by_col);
+ }
+ });
+}
+
+std::unique_ptr<MockRowset> MockRowset::select(const std::vector<std::string>&
cols, const std::function<bool(const MockRow&)>& condition, const std::string&
order_by_col, bool order_ascending) {
+ if (!order_by_col.empty()) {
+ sort(order_by_col, order_ascending);
+ }
+
+ std::unique_ptr<MockRowset> rowset;
+ if (cols.empty()) {
+ rowset = utils::make_unique<MockRowset>(column_names_, column_types_);
+ } else {
+ std::vector<DataType> col_types;
+ for (const auto& col : cols) {
+ col_types.push_back(column_types_.at(getColumnIndex(col)));
+ }
+ rowset = utils::make_unique<MockRowset>(cols, col_types);
+ }
+
+ std::vector<std::string> used_cols = cols.empty() ? column_names_ : cols;
+ for (const auto& row : rows_) {
+ if (condition(row)) {
+ std::vector<std::string> values;
+ for (const auto& col : used_cols) {
+ values.push_back(row.getValue(col));
+ }
+ rowset->addRow(values);
+ }
+ }
+
+ return rowset;
+}
+
+std::unique_ptr<Rowset> MockDB::execute(const std::string& query, const
std::vector<std::string>& args) {
+ if (minifi::utils::StringUtils::startsWith(query, "create table")) {
+ createTable(query);
+ } else if (minifi::utils::StringUtils::startsWith(query, "insert into")) {
+ insertInto(query, args);
+ } else if (minifi::utils::StringUtils::startsWith(query, "select")) {
+ return select(query, args);
+ } else {
+ throw std::runtime_error("Unknown query type");
+ }
+
+ return nullptr;
+}
+
+void MockDB::createTable(const std::string& query) {
+ std::smatch match;
+ std::regex expr("create table (\\w+)\\s*\\((.*)\\);");
+ std::regex_search(query, match, expr);
+ std::string table_name = match[1];
+ auto columns_with_type =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(match[2], ",");
+ std::vector<std::string> col_names;
+ std::vector<DataType> col_types;
+ for (const auto& col_with_type : columns_with_type) {
+ auto splitted =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(col_with_type, " ");
+ col_names.push_back(splitted[0]);
+ col_types.push_back(stringToDataType(splitted[1]));
+ }
+ tables_.emplace(table_name, MockRowset{col_names, col_types});
+ storeDb();
+}
+
+void MockDB::insertInto(const std::string& query, const
std::vector<std::string>& args) {
+ std::string replaced_query = query;
+ for (const auto& arg : args) {
+ replaced_query = minifi::utils::StringUtils::replaceOne(replaced_query,
"?", arg);
+ }
+
+ std::smatch match;
+ std::regex expr("insert into
(\\w+)\\s*(\\((.*)\\))*\\s*values\\s*\\((.+)\\)");
+ std::regex_search(replaced_query, match, expr);
+ std::string table_name = match[1];
+ std::vector<std::string> values =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(match[4], ",");
+ for (auto& value : values) {
+ value = minifi::utils::StringUtils::removeFramingCharacters(value, '\'');
+ }
+ auto insert_col_names =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(match[3], ",");
+ if (!insert_col_names.empty()) {
+ auto col_names = tables_.at(table_name).getColumnNames();
+ std::vector<std::string> row;
+ for (const auto& col_name : col_names) {
+ auto it = std::find(insert_col_names.begin(), insert_col_names.end(),
col_name);
+ if (it != insert_col_names.end()) {
+ row.push_back(values.at(it-insert_col_names.begin()));
+ } else {
+ row.push_back("NULL");
+ }
+ }
+ tables_.at(table_name).addRow(row);
+ } else {
+ tables_.at(table_name).addRow(values);
+ }
+
+ storeDb();
+}
+
+std::unique_ptr<Rowset> MockDB::select(const std::string& query, const
std::vector<std::string>& args) {
+ std::string replaced_query = query;
+ for (const auto& arg : args) {
+ replaced_query = minifi::utils::StringUtils::replaceOne(replaced_query,
"?", arg);
+ }
+
+ std::smatch match;
+ std::regex expr("select\\s+(.+)\\s+from\\s+(\\w+)\\s*(where ((.+(?= order
by))|.+$))*\\s*(order by (.+))*");
+ std::regex_search(replaced_query, match, expr);
+ auto cols = minifi::utils::StringUtils::splitAndTrimRemovingEmpty(match[1],
",");
+ if (cols[0] == "*") {
+ cols = {};
+ }
+ std::string table_name = match[2];
+ std::string condition_str = match[4];
+ auto condition = parseWhereCondition(condition_str);
+ std::string order = match[7];
+ std::string order_col;
+ bool descending = false;
+ if (!order.empty()) {
+ auto order_col_and_sort =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(order, " ");
+ order_col = order_col_and_sort[0];
+ descending = order_col_and_sort[1] == "desc";
+ }
+ return tables_.at(table_name).select(cols, condition, order_col,
!descending);
+}
+
+std::function<bool(const MockRow&)> MockDB::parseWhereCondition(const
std::string& full_condition_str) {
+ if (full_condition_str.empty()) {
+ return [](const MockRow&){ return true; };
+ }
+
+ auto condition_strings =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(full_condition_str,
"and");
+ std::vector<std::function<bool(const MockRow&)>> condition_parts;
+ for (const auto& condition_str : condition_strings) {
+ if (condition_str.find(">") != std::string::npos) {
+ auto elements =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(condition_str, ">");
+ condition_parts.push_back([elements](const MockRow& row){ return
std::stoi(row.getValue(elements[0])) > std::stoi(elements[1]); });
+ } else if (condition_str.find("<") != std::string::npos) {
+ auto elements =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(condition_str, "<");
+ condition_parts.push_back([elements](const MockRow& row){ return
std::stoi(row.getValue(elements[0])) < std::stoi(elements[1]); });
+ } else if (condition_str.find("=") != std::string::npos) {
+ auto elements =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(condition_str, "=");
+ condition_parts.push_back([elements](const MockRow& row) {
+ if (row.getDataType(elements[0]) == DataType::STRING) {
+ return row.getValue(elements[0]) ==
minifi::utils::StringUtils::removeFramingCharacters(elements[1], '"');
+ } else {
+ return std::stoi(row.getValue(elements[0])) ==
std::stoi(elements[1]);
+ }
+ });
+ } else {
+ throw std::runtime_error("Unimplemented WHERE condition");
+ }
+ }
+
+ return [condition_parts](const MockRow& row) {
+ bool result = true;
+ for (const auto& condition : condition_parts) {
+ result = result && condition(row);
+ }
+ return result;
+ };
+}
+
+void MockDB::readDb() {
+ tables_.clear();
+ std::ifstream file(file_path_);
+ std::string line;
+ ParsePhase phase = ParsePhase::NEW_TABLE;
+ std::string table_name;
+ std::vector<std::string> column_names;
+ std::vector<DataType> column_types;
+ while (std::getline(file, line)) {
+ switch (phase) {
+ case ParsePhase::NEW_TABLE: {
+ table_name = minifi::utils::StringUtils::trim(line);
+ phase = ParsePhase::COLUMN_NAMES;
+ break;
+ }
+ case ParsePhase::COLUMN_NAMES: {
+ column_names =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(line, "|");
+ phase = ParsePhase::COLUMN_TYPES;
+ break;
+ }
+ case ParsePhase::COLUMN_TYPES: {
+ column_types.clear();
+ auto type_strs =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(line, "|");
+ for (const auto& type : type_strs) {
+ column_types.push_back(stringToDataType(type));
+ }
+ tables_.emplace(table_name, MockRowset(column_names, column_types));
+ phase = ParsePhase::ROW_VALUES;
+ break;
+ }
+ case ParsePhase::ROW_VALUES: {
+ line = minifi::utils::StringUtils::trim(line);
+ if (line.empty()) {
+ phase = ParsePhase::NEW_TABLE;
+ break;
+ }
+ auto cells =
minifi::utils::StringUtils::splitAndTrimRemovingEmpty(line, "|");
+ tables_.at(table_name).addRow(cells);
+ break;
+ }
+ }
+ }
+}
+
+void MockDB::storeDb() {
+ std::ofstream file(file_path_);
+ for (const auto& table : tables_) {
+ file << table.first << std::endl;
+ auto& rowset = table.second;
+ for (const auto& col_name : rowset.getColumnNames()) {
+ file << col_name << "|";
+ }
+ file << std::endl;
+ for (const auto& col_type : rowset.getColumnTypes()) {
+ file << dataTypeToString(col_type) << "|";
+ }
+ file << std::endl;
+ for (const auto& row : rowset.getRows()) {
+ for (const auto& cell : row.getValues()) {
+ file << cell << "|";
+ }
+ file << std::endl;
+ }
+ file << std::endl;
+ }
+}
+
+DataType MockDB::stringToDataType(const std::string& type_str) {
+ if (type_str == "integer") return DataType::INTEGER;
+ if (type_str == "text") return DataType::STRING;
+ if (type_str == "real") return DataType::DOUBLE;
+ throw std::runtime_error("Unimplemented data type");
+}
+
+std::string MockDB::dataTypeToString(DataType data_type) {
+ switch (data_type) {
+ case DataType::INTEGER: return "integer";
+ case DataType::STRING: return "text";
+ case DataType::DOUBLE: return "real";
+ default: throw std::runtime_error("Unimplemented data type");
+ }
+}
+
+std::unique_ptr<Rowset> MockStatement::execute(const std::vector<std::string>&
args) {
+ MockDB db(file_path_);
+ return db.execute(query_, args);
+}
+
+MockODBCConnection::MockODBCConnection(std::string connectionString)
+ : connection_string_(std::move(connectionString)) {
+ std::smatch match;
+ std::regex expr("Database=(.*)(;|$)");
+ std::regex_search(connection_string_, match, expr);
+ file_path_ = match[1];
+}
+
+bool MockODBCConnection::connected(std::string& /*exception*/) const {
+ return true;
+}
+
+std::unique_ptr<sql::Statement> MockODBCConnection::prepareStatement(const
std::string& query) const {
+ return utils::make_unique<sql::MockStatement>(query, file_path_);
+}
+
+std::unique_ptr<Session> MockODBCConnection::getSession() const {
+ return utils::make_unique<sql::MockSession>();
+}
+
+} /* namespace sql */
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/libminifi/test/sql-tests/mocks/MockConnectors.h
b/libminifi/test/sql-tests/mocks/MockConnectors.h
new file mode 100644
index 0000000..f677f91
--- /dev/null
+++ b/libminifi/test/sql-tests/mocks/MockConnectors.h
@@ -0,0 +1,173 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+#pragma once
+
+#include <regex>
+#include <map>
+#include <vector>
+#include <algorithm>
+#include <memory>
+#include <string>
+
+#include "data/DatabaseConnectors.h"
+#include "utils/StringUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace sql {
+
+class MockRow : public Row {
+ public:
+ MockRow(std::vector<std::string>* column_names, std::vector<DataType>*
column_types, const std::vector<std::string>& column_values)
+ : column_names_(column_names), column_types_(column_types),
column_values_(column_values) {
+ }
+
+ std::size_t size() const override;
+ std::string getColumnName(std::size_t index) const override;
+ bool isNull(std::size_t index) const override;
+ DataType getDataType(std::size_t index) const override;
+ std::string getString(std::size_t index) const override;
+ double getDouble(std::size_t index) const override;
+ int getInteger(std::size_t index) const override;
+ long long getLongLong(std::size_t index) const override; // NOLINT Return
type comes from SOCI interface
+ unsigned long long getUnsignedLongLong(std::size_t index) const override;
// NOLINT Return type comes from SOCI interface
+ std::tm getDate(std::size_t /*index*/) const override;
+
+ std::vector<std::string> getValues() const;
+ std::string getValue(const std::string& col_name) const;
+ DataType getDataType(const std::string& col_name) const;
+
+ private:
+ std::vector<std::string>* column_names_;
+ std::vector<DataType>* column_types_;
+ std::vector<std::string> column_values_;
+};
+
+class MockRowset : public Rowset {
+ public:
+ MockRowset(const std::vector<std::string>& column_names, const
std::vector<DataType>& column_types)
+ : column_names_(column_names), column_types_(column_types) {
+ }
+
+ void addRow(const std::vector<std::string>& column_values);
+ void reset() override;
+ bool is_done() override;
+ Row& getCurrent() override;
+ void next() override;
+
+ std::vector<std::string> getColumnNames() const;
+ std::vector<DataType> getColumnTypes() const;
+ std::vector<MockRow> getRows() const;
+ std::size_t getColumnIndex(const std::string& col_name) const;
+ void sort(const std::string& order_by_col, bool order_ascending = true);
+ std::unique_ptr<MockRowset> select(const std::vector<std::string>& cols,
const std::function<bool(const MockRow&)>& condition, const std::string&
order_by_col, bool order_ascending = true);
+
+ private:
+ std::vector<std::string> column_names_;
+ std::vector<DataType> column_types_;
+ std::vector<MockRow> rows_;
+ std::vector<MockRow>::iterator current_row_;
+};
+
+class MockDB {
+ public:
+ explicit MockDB(const std::string& file_path) : file_path_(file_path) {
+ readDb();
+ }
+
+ ~MockDB() {
+ storeDb();
+ }
+
+ std::unique_ptr<Rowset> execute(const std::string& query, const
std::vector<std::string>& args);
+
+ private:
+ enum class ParsePhase {
+ NEW_TABLE,
+ COLUMN_NAMES,
+ COLUMN_TYPES,
+ ROW_VALUES
+ };
+
+ void createTable(const std::string& query);
+ void insertInto(const std::string& query, const std::vector<std::string>&
args);
+ std::unique_ptr<Rowset> select(const std::string& query, const
std::vector<std::string>& args);
+ void readDb();
+ void storeDb();
+
+ /**
+ * This function parses an and-separated list of conditions in the format of
<column_name> [<>=] <value>
+ * @param condition_str SQL WHERE condition string with only AND logical
operators
+ * @return Function object evaluating MockRow according to the condition
parameter
+ */
+ std::function<bool(const MockRow&)> parseWhereCondition(const std::string&
condition_str);
+
+ static DataType stringToDataType(const std::string& type_str);
+ static std::string dataTypeToString(DataType data_type);
+
+ std::string file_path_;
+ std::map<std::string, MockRowset> tables_;
+};
+
+class MockStatement : public Statement {
+ public:
+ explicit MockStatement(const std::string& query, const std::string&
file_path)
+ : Statement(minifi::utils::StringUtils::toLower(query)),
file_path_(file_path) {
+ }
+
+ std::unique_ptr<Rowset> execute(const std::vector<std::string>& args = {})
override;
+
+ private:
+ std::string file_path_;
+};
+
+class MockSession : public Session {
+ public:
+ void begin() override {
+ }
+
+ void commit() override {
+ }
+
+ void rollback() override {
+ }
+
+ void execute(const std::string& /*statement*/) override {
+ }
+};
+
+class MockODBCConnection : public Connection {
+ public:
+ explicit MockODBCConnection(std::string connectionString);
+ bool connected(std::string& exception) const override;
+ std::unique_ptr<sql::Statement> prepareStatement(const std::string& query)
const override;
+ std::unique_ptr<Session> getSession() const override;
+
+ private:
+ std::string connection_string_;
+ std::string file_path_;
+};
+
+} /* namespace sql */
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/libminifi/test/sql-tests/mocks/MockODBCService.h
b/libminifi/test/sql-tests/mocks/MockODBCService.h
new file mode 100644
index 0000000..6b60ab9
--- /dev/null
+++ b/libminifi/test/sql-tests/mocks/MockODBCService.h
@@ -0,0 +1,67 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "MockConnectors.h"
+#include "services/DatabaseService.h"
+#include "core/Resource.h"
+#include "data/DatabaseConnectors.h"
+#include "utils/GeneralUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace sql {
+namespace controllers {
+
+class MockODBCService : public DatabaseService {
+ public:
+ explicit MockODBCService(const std::string &name, utils::Identifier uuid =
utils::Identifier())
+ : DatabaseService(name, uuid),
+ logger_(logging::LoggerFactory<MockODBCService>::getLogger()) {
+ initialize();
+ }
+
+ explicit MockODBCService(const std::string &name, const
std::shared_ptr<Configure> &configuration)
+ : DatabaseService(name),
+ logger_(logging::LoggerFactory<MockODBCService>::getLogger()) {
+ setConfiguration(configuration);
+ initialize();
+ }
+
+ std::unique_ptr<sql::Connection> getConnection() const {
+ return
minifi::utils::make_unique<sql::MockODBCConnection>(connection_string_);
+ }
+
+ private:
+ std::shared_ptr<logging::Logger> logger_;
+};
+
+REGISTER_RESOURCE(MockODBCService, "Controller service that provides Mock ODBC
database connection");
+
+} /* namespace controllers */
+} /* namespace sql */
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/libminifi/test/unit/StringUtilsTests.cpp
b/libminifi/test/unit/StringUtilsTests.cpp
index c48df4a..38b2f83 100644
--- a/libminifi/test/unit/StringUtilsTests.cpp
+++ b/libminifi/test/unit/StringUtilsTests.cpp
@@ -55,9 +55,71 @@ TEST_CASE("TestStringUtils::split5", "[test split with
delimiter set to empty st
REQUIRE(expected == StringUtils::split("hello world", ""));
}
-TEST_CASE("TestStringUtils::splitTrasformed", "[test split with trim]") {
+TEST_CASE("TestStringUtils::split6", "[test split with delimiter with empty
results]") {
+ std::vector<std::string> expected = {""};
+ REQUIRE(expected == StringUtils::split("", ","));
+ expected = {"", ""};
+ REQUIRE(expected == StringUtils::split(",", ","));
+ expected = {"", " ", "", ""};
+ REQUIRE(expected == StringUtils::split(", ,,", ","));
+}
+
+TEST_CASE("TestStringUtils::splitRemovingEmpty", "[test splitRemovingEmpty
multiple delimiter]") {
+ std::vector<std::string> expected = { "hello", "world", "I'm", "a", "unit",
"test" };
+ REQUIRE(expected == StringUtils::split("hello world I'm a unit test", " "));
+}
+
+TEST_CASE("TestStringUtils::splitRemovingEmpty2", "[test splitRemovingEmpty no
delimiter]") {
+ std::vector<std::string> expected = { "hello" };
+ REQUIRE(expected == StringUtils::splitRemovingEmpty("hello", ","));
+}
+
+TEST_CASE("TestStringUtils::splitRemovingEmpty3", "[test splitRemovingEmpty
with delimiter with empty results]") {
+ std::vector<std::string> expected = {};
+ REQUIRE(expected == StringUtils::splitRemovingEmpty("", ","));
+ REQUIRE(expected == StringUtils::splitRemovingEmpty(",", ","));
+ expected = {" "};
+ REQUIRE(expected == StringUtils::splitRemovingEmpty(", ,,", ","));
+}
+
+TEST_CASE("TestStringUtils::splitAndTrim", "[test split with trim with
characters]") {
std::vector<std::string> expected{ "hello", "world peace" };
REQUIRE(expected == StringUtils::splitAndTrim("hello, world peace", ","));
+ expected = {""};
+ REQUIRE(expected == StringUtils::splitAndTrim("", ","));
+ expected = {"", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim(",", ","));
+ expected = {"", "", "", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim(", ,,", ","));
+}
+
+TEST_CASE("StringUtils::splitAndTrim2", "[test split with trim with words]") {
+ std::vector<std::string> expected{ "tom", "jerry" };
+ REQUIRE(expected == StringUtils::splitAndTrim("tom and jerry", "and"));
+ expected = {"", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim("and", "and"));
+ expected = {"", "", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim("andand", "and"));
+ expected = {"stan", "pan", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim("stan and pan and ", "and"));
+ expected = {"", ""};
+ REQUIRE(expected == StringUtils::splitAndTrim(" and ", "and"));
+ expected = {"a", "b", "c"};
+ REQUIRE(expected == StringUtils::splitAndTrim("a and ... b and ... c", "and
..."));
+}
+
+TEST_CASE("StringUtils::splitAndTrimRemovingEmpty", "[test split with trim
removing empty strings]") {
+ std::vector<std::string> expected{ "tom", "jerry" };
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty("tom and jerry",
"and"));
+ expected = {};
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty("and", "and"));
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty("andand", "and"));
+ expected = {"stan", "pan"};
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty("stan and pan and
", "and"));
+ expected = {};
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty(" and ", "and"));
+ expected = {"a", "b", "c"};
+ REQUIRE(expected == StringUtils::splitAndTrimRemovingEmpty("a and ... b and
... c", "and ..."));
}
TEST_CASE("StringUtils::replaceEnvironmentVariables works correctly",
"[replaceEnvironmentVariables]") {
diff --git a/win_build_vs.bat b/win_build_vs.bat
index 9975db4..62d0697 100755
--- a/win_build_vs.bat
+++ b/win_build_vs.bat
@@ -40,6 +40,7 @@ set redist=
set build_linter=OFF
set build_nanofi=OFF
set build_opencv=OFF
+set real_odbc=OFF
set arg_counter=0
for %%x in (%*) do (
@@ -65,12 +66,13 @@ for %%x in (%*) do (
if [%%~x] EQU [/CI] set
"strict_gsl_checks=-DSTRICT_GSL_CHECKS=AUDIT" & set test_custom_wel_provider=ON
if [%%~x] EQU [/NONFREEUCRT] set "redist=-DMSI_REDISTRIBUTE_UCRT_NONASL=ON"
if [%%~x] EQU [/L] set build_linter=ON
+ if [%%~x] EQU [/RO] set real_odbc=ON
)
mkdir %builddir%
pushd %builddir%\
-cmake -G %generator% -A %build_platform%
-DINSTALLER_MERGE_MODULES=%installer_merge_modules%
-DTEST_CUSTOM_WEL_PROVIDER=%test_custom_wel_provider% -DENABLE_SQL=%build_SQL%
-DCMAKE_BUILD_TYPE_INIT=%cmake_build_type%
-DCMAKE_BUILD_TYPE=%cmake_build_type% -DWIN32=WIN32
-DENABLE_LIBRDKAFKA=%build_kafka% -DENABLE_JNI=%build_jni% -DOPENSSL_OFF=OFF
-DENABLE_COAP=%build_coap% -DENABLE_AWS=%build_AWS% -DENABLE_PDH=%build_PDH%
-DENABLE_AZURE=%build_azure% -DENABLE_SFTP=%build_SFTP% -DENABLE_NANOFI [...]
+cmake -G %generator% -A %build_platform%
-DINSTALLER_MERGE_MODULES=%installer_merge_modules%
-DTEST_CUSTOM_WEL_PROVIDER=%test_custom_wel_provider% -DENABLE_SQL=%build_SQL%
-DUSE_REAL_ODBC_TEST_DRIVER=%real_odbc%
-DCMAKE_BUILD_TYPE_INIT=%cmake_build_type%
-DCMAKE_BUILD_TYPE=%cmake_build_type% -DWIN32=WIN32
-DENABLE_LIBRDKAFKA=%build_kafka% -DENABLE_JNI=%build_jni% -DOPENSSL_OFF=OFF
-DENABLE_COAP=%build_coap% -DENABLE_AWS=%build_AWS% -DENABLE_PDH=%build_PDH%
-DENABLE_AZURE=%build_azure% -D [...]
IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
if [%cpack%] EQU [ON] (
cpack -C %cmake_build_type%