diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f91f646
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,12 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# Linux start script should use lf
+/gradlew text eol=lf
+
+# These are Windows script files and should use crlf
+*.bat text eol=crlf
+
+# Binary files should be left untouched
+*.jar binary
+
diff --git a/.gitignore b/.gitignore
index d4fb281..898b0ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,41 +1,38 @@
-# Prerequisites
-*.d
-
-# Compiled Object files
-*.slo
-*.lo
-*.o
-*.obj
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Linker files
-*.ilk
-
-# Debugger Files
-*.pdb
-
-# Compiled Dynamic libraries
-*.so
-*.dylib
-*.dll
-
-# Fortran module files
-*.mod
-*.smod
-
-# Compiled Static libraries
-*.lai
-*.la
-*.a
-*.lib
-
-# Executables
-*.exe
-*.out
-*.app
-
-# debug information files
-*.dwo
+# Java build artifacts
+*.class
+*.log
+out/
+build/
+
+# VS Code settings (keep launch.json)
+
+# OS-specific files
+.DS_Store
+Thumbs.db
+
+# ignore lib folder except .jar files
+libs/*
+
+# ignore Gradle project-specific cache directory
+.gradle
+
+# ignore Gradle build output directory
+build
+
+
+# ignore resources
+resources/project/
+
+# ignore the presentation and report
+project_presentation
+project_report
+
+# ignore the polypheny-all.jar and app.jar files
+app\build\libs
+
+# ignore the matlab add-on folder structure
+matlab-polypheny-connector/jar/polypheny-all.jar
+matlab-polypheny-connector/resources
+matlab-polypheny-connector/libs/polypheny-jdbc-driver-2.3.jar
+matlab-polypheny-connector/Toolbox1/PolyphenyConnector.mltbx
+matlab-polypheny-connector/Toolbox1/release/*.mltbx
diff --git a/.vscode/Polypheny-Style.xml b/.vscode/Polypheny-Style.xml
new file mode 100644
index 0000000..e8365c4
--- /dev/null
+++ b/.vscode/Polypheny-Style.xml
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..3865e57
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,24 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Main",
+ "request": "launch",
+ "mainClass": "Main",
+ "projectName": "Matlab_Connector"
+ },
+ {
+ "type": "java",
+ "name": "PolyphenyConnection",
+ "request": "launch",
+ "mainClass": "PolyphenyConnection",
+ "projectName": "Learning Contract_7fe425cb",
+ "vmArgs": "-Djava.library.path=C:\\Programme\\MATLAB\\R2025a\\bin\\win64",
+ "env": {
+ "PATH": "C:\\Programme\\MATLAB\\R2025a\\bin\\win64;C:\\Programme\\MATLAB\\R2025a\\extern\\bin\\win64;${env:PATH}"
+ },
+ "console": "internalConsole"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..282e471
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,48 @@
+{
+ "java.project.sourcePaths": [
+ "app/src/main/java"
+ ],
+ "java.project.referencedLibraries": [],
+ "cSpell.words": [
+ "Afterall",
+ "disp",
+ "elems",
+ "empid",
+ "emps",
+ "HASPOLYPHENY",
+ "Insertand",
+ "iscell",
+ "ischar",
+ "isscalar",
+ "istable",
+ "JDBC",
+ "johnrengelman",
+ "Matlabtoolbox",
+ "mongoql",
+ "mongotest",
+ "mtlbx",
+ "Multimodel",
+ "multiquery",
+ "myexecutor",
+ "mypath",
+ "Polpyheny's",
+ "polyconnection",
+ "Polypheny",
+ "polyphenyconnector",
+ "POLYWRAPPER",
+ "QUERYLIST",
+ "QUERYSTR",
+ "stringbuilder",
+ "varargin",
+ "VARCHAR",
+ "wrongpass",
+ "wronguser"
+ ],
+ "java.format.enabled": true,
+ "java.format.settings.url": ".vscode/Polypheny-Style.xml",
+ "java.format.settings.profile": "Polypheny Code Style",
+ "[java]": {
+ "editor.defaultFormatter": "redhat.java",
+ "editor.formatOnSave": true
+ }
+}
diff --git a/LICENSE b/LICENSE
index 261eeb9..845ef47 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright [2025] [Fynn Gohlke]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/Polypheny_Connector.prj b/Polypheny_Connector.prj
new file mode 100644
index 0000000..6b95f98
--- /dev/null
+++ b/Polypheny_Connector.prj
@@ -0,0 +1,2 @@
+
+
diff --git a/app/bin/main/.gitignore b/app/bin/main/.gitignore
new file mode 100644
index 0000000..b733afd
--- /dev/null
+++ b/app/bin/main/.gitignore
@@ -0,0 +1,14 @@
+# Java build artifacts
+*.class
+*.log
+out/
+build/
+
+# VS Code settings (keep launch.json)
+
+# OS-specific files
+.DS_Store
+Thumbs.db
+
+# Ignore lib folder except .jar files
+lib/*
\ No newline at end of file
diff --git a/app/bin/main/ClassTest.m b/app/bin/main/ClassTest.m
new file mode 100644
index 0000000..47f32fc
--- /dev/null
+++ b/app/bin/main/ClassTest.m
@@ -0,0 +1,9 @@
+clear all; clear classes
+%conn = javaObject('polyphenyconnector.PolyphenyConnection', 'localhost', int32(20590), 'pa', '');
+%exec = javaObject('polyphenyconnector.QueryExecutor', conn);
+
+%res = exec.execute('sql', 'SELECT 1 AS x');
+results = runtests('PolyphenyWrapperTest');
+disp(results)
+
+%disp(res);
diff --git a/app/bin/main/LICENSE b/app/bin/main/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/app/bin/main/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/app/bin/main/Polypheny.m b/app/bin/main/Polypheny.m
new file mode 100644
index 0000000..dfd0721
--- /dev/null
+++ b/app/bin/main/Polypheny.m
@@ -0,0 +1,138 @@
+classdef Polypheny < handle
+% POLYPHENY MATLAB wrapper for the Polypheny Java connector. Wraps polyphenyconnector.PolyphenyConnection
+% and polyphenyconnector.QueryExecutor to run queries from MATLAB
+ properties ( Access = private )
+ polyConnection % Java PolyphenyConnection
+ queryExecutor % Java QueryExecutor
+
+ end
+
+ methods
+
+ function PolyWrapper = Polypheny( host, port, user, password )
+ % Polypheny( LANGUAGE, HOST, PORT, USER, PASSWORD ): Set up Java connection + executor
+ % LANGUAGE: The database language ( 'sql', 'mongo', 'cypher' )
+ % HOST: Database host ( e.g. 'localhost' )
+ % PORT: Database port ( integer )
+ % USER: Username
+ % PASSWORD: Password
+
+ % This makes sure that Matlab sees Java classes supplied by the .jar files in the Matlabtoolbox PolyphenyConnector.mtlbx
+ try
+ if ~polypheny.Polypheny.hasPolypheny( )
+ startup( );
+ end
+ PolyWrapper.polyConnection = javaObject( "polyphenyconnector.PolyphenyConnection",host, int32( port ), user, password );
+ PolyWrapper.queryExecutor = javaObject( "polyphenyconnector.QueryExecutor", PolyWrapper.polyConnection );
+
+ catch ME %Matlab Exception
+ disp( "Error: " + ME.message )
+ end
+
+ end
+
+
+
+ function matlab_result = query( PolyWrapper, language, namespace, queryStr )
+ % query( POLYWRAPPER, QUERYSTR ): Execute query via QueryExecutor.java
+ % POLYWRAPPER: The PolyWrapper Matlab object
+ % LANGUAGE: The language of the query string -> SQL, mongo, Cypher
+ % QUERYSTR: The queryStr set by the user
+ % @return matlab_result: The result of the query -> return type differs for SQL,Mongo and Cypher
+
+
+ try
+ java_result = PolyWrapper.queryExecutor.execute( string( language ), string( namespace ), queryStr );
+
+ switch lower( language )
+ case "sql"
+ if isempty( java_result )
+ matlab_result = [];
+ elseif isscalar( java_result )
+ matlab_result = java_result;
+ elseif isa( java_result,'java.lang.Object[]' ) && numel( java_result )==2
+ tmp = cell( java_result );
+ colNames = cell( tmp{1} );
+ data = cell( tmp{2} );
+ matlab_result = cell2table( data, 'VariableNames', colNames );
+ else
+ matlab_result = [];
+ end
+
+ case "mongo"
+ if isa( java_result, 'java.util.List' )
+ % Current driver behavior: always returns List of JSON docs
+ matlab_result = string(java_result);
+ elseif isnumeric( java_result )
+ % Not observed in current driver, but kept for forward compatibility
+ % (e.g. if Polypheny ever returns scalar counts directly)
+ matlab_result = java_result;
+ else
+ error( "Unexpected Mongo result type: %s", class( java_result ) );
+ end
+
+ case "cypher"
+ % TODO: integrate once Cypher executor is ready
+ error( "Cypher not supported yet." );
+
+ otherwise
+ error( "Unsupported language: %s", language );
+ end
+
+ catch ME
+ error( "Query execution failed: %s", ME.message );
+ end
+ end
+
+ function matlab_result = queryBatch( PolyWrapper, language, namespace, queryList )
+ % queryBatch( POLYWRAPPER, QUERYLIST ): Execute batch of non-SELECT statements
+ % QUERYLIST: A cell array of SQL strings ( INSERT, UPDATE, DELETE, etc. )
+ %
+ % Returns: int array with rows affected per statement
+
+ if ~iscell( queryList )
+ error( 'queryBatch expects a cell array of query strings' );
+ end
+
+ javaList = java.util.ArrayList();
+ for i = 1:numel( queryList )
+ javaList.add( string(queryList{i} ) );
+ end
+
+ switch lower(language)
+ case "sql"
+ java_result = PolyWrapper.queryExecutor.executeBatchSql( javaList );
+ %matlab_result = double(java_result(:))';
+ vals = double(java_result(:)); % convert Java int[] to MATLAB column vector
+ matlab_result = array2table(vals, 'VariableNames', {'RowsAffected'});
+
+ case "mongo"
+ java_result = PolyWrapper.queryExecutor.executeBatchMongo( string(namespace), javaList );
+ matlab_result = string( java_result ); % outer list
+
+ case "cypher"
+ error( "Batch execution for Cypher not yet implemented." );
+
+ otherwise
+ error( "Unsupported language: %s", language );
+ end
+
+ end
+
+ function close( PolyWrapper )
+ % close( POLYWRAPPER ): Close the Java connection
+ % POLYWRAPPER: The PolyWrapper Matlab object
+ PolyWrapper.polyConnection.close( );
+ end
+ end
+
+ methods ( Static )
+ function flag = hasPolypheny( )
+ % HASPOLYPHENY Returns true if Polypheny Java classes are available because the exist( 'polyphenyconnector.PolyphenyConnection','class' )
+ % returns 8 if Matlab sees the Java class and 0 otherwise.
+ flag = ( exist( 'polyphenyconnector.PolyphenyConnection','class' ) == 8 );
+ end
+
+ end
+
+end
diff --git a/app/bin/main/PolyphenyWrapperTest.m b/app/bin/main/PolyphenyWrapperTest.m
new file mode 100644
index 0000000..68b1909
--- /dev/null
+++ b/app/bin/main/PolyphenyWrapperTest.m
@@ -0,0 +1,69 @@
+classdef PolyphenyWrapperTest < matlab.unittest.TestCase
+ properties
+ conn
+ end
+
+ methods (TestMethodSetup)
+ function setupConnection(testCase)
+ testCase.conn = polypheny.Polypheny('localhost', int32(20590), 'pa', '' );
+ end
+ end
+
+ methods (TestMethodTeardown)
+ function closeConnection(testCase)
+ testCase.conn.close();
+ end
+ end
+
+ methods (Test)
+ function testScalar(testCase)
+ r = testCase.conn.query( "sql" , "" , "SELECT 1 AS x");
+ testCase.verifyEqual(r, 1);
+ end
+
+ function testTable(testCase)
+ testCase.conn.query("sql" , "" , "DROP TABLE IF EXISTS wrapper_test");
+ testCase.conn.query("sql" , "" , "CREATE TABLE wrapper_test (id INTEGER PRIMARY KEY, name VARCHAR)");
+ testCase.conn.query("sql" , "" , "INSERT INTO wrapper_test VALUES (1,'Alice'),(2,'Bob')");
+
+ T = testCase.conn.query("sql" , "" , "SELECT * FROM wrapper_test ORDER BY id");
+
+ if istable(T)
+ % Expected: table output with column "name"
+ testCase.verifyEqual(T.name, {'Alice'; 'Bob'});
+ elseif iscell(T)
+ % Fallback: check the raw cell contents
+ testCase.verifyEqual(T(:,2), {'Alice','Bob'}');
+ else
+ testCase.verifyFail("Unexpected return type: " + class(T));
+ end
+ end
+
+ function testEmpty(testCase)
+ T = testCase.conn.query("sql" , "" , "SELECT * FROM wrapper_test WHERE id=999");
+ testCase.verifyEmpty(T);
+ end
+
+ function testBatchInsert(testCase)
+ % Prepare table
+ testCase.conn.query("sql" , "" , "DROP TABLE IF EXISTS batch_test");
+ testCase.conn.query("sql" , "" , "CREATE TABLE batch_test (id INTEGER PRIMARY KEY, name VARCHAR)");
+
+ % Batch insert 2 rows
+ queries = { ...
+ "INSERT INTO batch_test VALUES (1,'Alice')", ...
+ "INSERT INTO batch_test VALUES (2,'Bob')" ...
+ };
+ result = testCase.conn.queryBatch("sql" , "" , queries);
+
+ % Verify JDBC return codes
+ testCase.verifyEqual(result.RowsAffected', [1 1]);
+
+ % Verify table contents
+ T = testCase.conn.query("sql" , "" ,"SELECT id, name FROM batch_test ORDER BY id");
+ testCase.verifyEqual(T.id, [1; 2]);
+ testCase.verifyEqual(string(T.name), ["Alice"; "Bob"]);
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/app/bin/main/PolyphenyWrapperTestMQL.m b/app/bin/main/PolyphenyWrapperTestMQL.m
new file mode 100644
index 0000000..e992fc1
--- /dev/null
+++ b/app/bin/main/PolyphenyWrapperTestMQL.m
@@ -0,0 +1,245 @@
+classdef PolyphenyWrapperTestMQL < matlab.unittest.TestCase
+ properties
+ conn
+ end
+
+ methods(TestClassSetup)
+ function setUpNamespaceAndCollection(testCase)
+ clc;
+ % open connection once
+ testCase.conn = polypheny.Polypheny("localhost",20590,"pa","");
+
+ % try create collection
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.createCollection("unittest_collection")');
+ catch
+ end
+ end
+ end
+
+ methods(TestClassTeardown)
+ function tearDownNamespaceAndCollection(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.drop()');
+ catch
+ end
+ testCase.conn.close();
+ end
+ end
+
+ methods(TestMethodSetup)
+ function clearCollection(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.deleteMany({})');
+ catch
+ end
+ end
+ end
+
+ methods(TestMethodTeardown)
+ function clearCollectionAfter(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.deleteMany({})');
+ catch
+ end
+ end
+ end
+
+ methods(Test)
+
+ function testDeleteManyRemovesAllDocs(testCase)
+ % Drop & recreate collection
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.drop()');
+ testCase.conn.query("mongo","mongotest", 'db.createCollection("unittest_collection")');
+
+ % Insert three documents
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})');
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})');
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":3,"name":"Ciri"})');
+
+ % Call deleteMany({})
+ ack = testCase.conn.query("mongo","mongotest", 'db.unittest_collection.deleteMany({})');
+ disp("Ack from deleteMany:");
+ disp(ack);
+
+ % Verify collection is empty
+ docs = testCase.conn.query("mongo","mongotest", 'db.unittest_collection.find({})');
+ docs = jsondecode(docs);
+ testCase.verifyEmpty(docs, "Collection should be empty after deleteMany({})");
+ end
+
+ function testInsertManyAndNestedDocument(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":14})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":20})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":24})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":30,"adress":{"Country":"Switzerland","Code":4051}})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({"age":{$gt:29}})');
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"age":30'));
+ end
+
+ function testBooleanField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"flag":true})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyTrue(contains(docs(1),'"flag":true'));
+ testCase.verifyClass(decoded.flag, 'logical'); % asserts that class(decoded.flag) == logical
+ end
+
+ function testIntegerAgeField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":42})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"age":42'));
+ end
+
+ function testStringField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"name":"Alice"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"name":"Alice"'));
+ end
+
+ function testLongField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"big":1111111111111111111})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"big":1111111111111111111'));
+ end
+
+ function testDoubleField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"pi":3.14159})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"pi":3.14159'));
+ end
+
+ function testInsertAndQueryTwoDocsRawJson(testCase)
+ % Clean collection
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.drop()');
+ testCase.conn.query("mongo","mongotest",'db.createCollection("unittest_collection")');
+
+ % Insert two docs
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})');
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})');
+
+ % Query back
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp("Raw JSON:");
+ disp(docs);
+ decoded = jsondecode(docs);
+ disp(decoded)
+
+ % Assert raw JSON is exactly what we want
+ testCase.verifyTrue(contains(docs(1),'"name":"Alice"'));
+ testCase.verifyTrue(contains(docs(1),'"name":"Bob"'));
+ end
+
+
+ function testCountDocuments(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"name":"Bob"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.countDocuments({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'{"count":1}'));
+ end
+
+ function testArrayField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"scores":[1,2,3]})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs,'"scores":[1,2,3]'));
+ end
+
+ function testFindOnEmptyCollection(testCase)
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(docs,"[]");
+ end
+
+ function testInsertManyAndFindMultiple(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"id":10,"name":"A"})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"id":11,"name":"B"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyTrue(contains(docs,'"id":10'))
+ testCase.verifyTrue(contains(docs,'"name":"A"'))
+ testCase.verifyTrue(contains(docs,'"id":11'))
+ testCase.verifyTrue(contains(docs,'"name":"B"'))
+ end
+
+ function testBatchInsertAndFind(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"name":"Alice","age":25})', ...
+ 'db.unittest_collection.insertOne({"name":"Alice","age":20})', ...
+ 'db.unittest_collection.insertOne({"name":"Bob","age":30})' };
+ ignore = testCase.conn.queryBatch("mongo","mongotest",queries);
+ queries2 = { ...
+ 'db.unittest_collection.find({"name":"Alice"})', ...
+ 'db.unittest_collection.find({"name":"Alice","age":20})', ...
+ 'db.unittest_collection.find({"name":"Bob","age":30})' };
+ docs = testCase.conn.queryBatch("mongo","mongotest", queries2);
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyEqual(numel(decoded{1}), 2); % 2 docs in first query
+
+ % check names
+ names = {decoded{1}.name}; % cell of names
+ disp(names)
+ testCase.verifyEqual(string(names), ["Alice","Alice"]);
+
+ end
+
+
+ function testBatchMixedOps(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"name":"Charlie","active":true})', ...
+ 'db.unittest_collection.countDocuments({})' };
+ docs = testCase.conn.queryBatch("mongo","mongotest",queries);
+ testCase.verifyEqual(numel(docs),1);
+ decoded = jsondecode(docs)
+ varname = fieldnames(decoded{2})
+ disp(decoded{2}.count)
+ testCase.verifyTrue(decoded{2}.count==1);
+ end
+
+ function testSyntaxErrorThrows(testCase)
+ badQuery = 'db.unittest_collection.insertOne({"foo":123)'; % invalid JSON
+ testCase.verifyError(@() testCase.conn.query("mongo","mongotest",badQuery),?MException);
+ end
+
+ function testMultiStatementFails(testCase)
+ badMulti = [ ...
+ 'db.people.insertOne({"name":"Alice","age":20}); ' ...
+ 'db.people.insertOne({"name":"Bob","age":24}); ' ...
+ 'db.people.find({})' ];
+ testCase.verifyError(@() testCase.conn.query("mongo","mongotest",badMulti),?MException);
+ end
+
+ function testBatchRollback(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})', ...
+ 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})', ...
+ 'db.unittest_collection.insertOne({"id":3,"name":"Janice"' }; % broken
+ testCase.verifyError(@() testCase.conn.queryBatch("mongo","mongotest",queries),?MException);
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyEqual(docs,"[]")
+ end
+
+ end
+end
diff --git a/app/bin/main/PolyphenyWrapperTestSQL.m b/app/bin/main/PolyphenyWrapperTestSQL.m
new file mode 100644
index 0000000..0b25e6b
--- /dev/null
+++ b/app/bin/main/PolyphenyWrapperTestSQL.m
@@ -0,0 +1,220 @@
+classdef PolyphenyWrapperTestSQL < matlab.unittest.TestCase
+ properties
+ conn % polypheny.Polypheny wrapper
+ end
+
+ methods(TestClassSetup)
+ function setUpNamespaceAndTable(testCase)
+ clc;
+ % Open connection once for all tests
+ testCase.conn = polypheny.Polypheny("localhost",20590,"pa","");
+
+ % Drop leftovers if they exist
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP NAMESPACE IF EXISTS unittest_namespace");
+ catch
+ end
+
+ % Create namespace + table for execute()
+ testCase.conn.query("sql","", ...
+ "CREATE NAMESPACE unittest_namespace");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "CREATE TABLE unittest_namespace.unittest_table (id INT NOT NULL, name VARCHAR(100), PRIMARY KEY(id))");
+
+ % Drop and recreate batch_table
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.batch_table");
+ catch
+ end
+ testCase.conn.query("sql","unittest_namespace", ...
+ "CREATE TABLE unittest_namespace.batch_table (" + ...
+ "emp_id INT NOT NULL, " + ...
+ "name VARCHAR(100), " + ...
+ "gender VARCHAR(10), " + ...
+ "birthday DATE, " + ...
+ "employee_id INT, " + ...
+ "PRIMARY KEY(emp_id))");
+ end
+ end
+
+ methods(TestClassTeardown)
+ function tearDownNamespaceAndTable(testCase)
+ % Cleanup after all tests
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.batch_table");
+ testCase.conn.query("sql","", ...
+ "DROP NAMESPACE IF EXISTS unittest_namespace");
+ catch
+ end
+ testCase.conn.close();
+ end
+ end
+
+ methods(TestMethodSetup)
+ function clearTables(testCase)
+ % Clear before each test
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.batch_table");
+ catch
+ end
+ end
+ end
+
+ methods(TestMethodTeardown)
+ function clearTablesAfter(testCase)
+ % Clear again after each test
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.batch_table");
+ catch
+ end
+ end
+ end
+
+ methods(Test)
+ function testScalarLiteral(testCase)
+ r = testCase.conn.query("sql","","SELECT 42 AS answer");
+ testCase.verifyEqual(r,42);
+ end
+
+ function testEmptyLiteral(testCase)
+ r = testCase.conn.query("sql","","SELECT * FROM (SELECT 1) t WHERE 1=0");
+ testCase.verifyEmpty(r);
+ end
+
+ function testTableLiteral(testCase)
+ r = testCase.conn.query("sql","unittest_namespace","SELECT 1 AS a, 2 AS b UNION ALL SELECT 3,4");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'a','b'});
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,2});
+ testCase.verifyEqual(table2cell(r(2,:)),{3,4});
+ end
+
+ function testInsert(testCase)
+ r = testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.verifyEqual(r,1);
+ end
+
+ function testInsertAndSelect(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id,name FROM unittest_namespace.unittest_table");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'id','name'});
+ testCase.verifyEqual(height(r),1);
+ testCase.verifyEqual(table2cell(r),{1,'Alice'});
+ end
+
+ function testScalarFromTable(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Carol')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id FROM unittest_namespace.unittest_table WHERE name='Carol'");
+ testCase.verifyEqual(r,2);
+ end
+
+ function testInsertAndSelectMultipleRows(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id,name FROM unittest_namespace.unittest_table ORDER BY id");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,'Alice'});
+ testCase.verifyEqual(table2cell(r(2,:)),{2,'Bob'});
+ end
+
+ function testDeleteFromTable(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+ testCase.conn.query("sql","unittest_namespace","DELETE FROM unittest_namespace.unittest_table");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT * FROM unittest_namespace.unittest_table");
+ testCase.verifyEmpty(r);
+ end
+
+ function testBatchInsertEmployees(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (2,'Bob','M',DATE '1989-05-12',1002)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (3,'Jane','F',DATE '1992-07-23',1003)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (4,'Tim','M',DATE '1991-03-03',1004)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (5,'Alex','M',DATE '1994-11-11',1005)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (6,'Mason','M',DATE '1988-04-22',1006)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (7,'Rena','F',DATE '1995-06-17',1007)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (8,'Christopher','M',DATE '1987-08-09',1008)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (9,'Lexi','F',DATE '1996-09-30',1009)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (10,'Baen','M',DATE '1990-10-05',1010)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (11,'Ricardo','M',DATE '1986-12-12',1011)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (12,'Tim','M',DATE '1993-02-02',1012)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (13,'Beya','F',DATE '1994-05-25',1013)"
+ };
+ counts = testCase.conn.queryBatch("sql","unittest_namespace",queries);
+ testCase.verifyEqual(height(counts),13);
+ disp(counts)
+ testCase.verifyTrue(all(counts.RowsAffected == 1));
+ r = testCase.conn.query("sql","unittest_namespace","SELECT COUNT(*) FROM unittest_namespace.batch_table");
+ testCase.verifyEqual(r,13);
+ end
+
+ function testBatchRollbackOnFailure(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "BROKEN QUERY"
+ };
+ testCase.verifyError(@() testCase.conn.queryBatch("sql","unittest_namespace",queries),?MException);
+ r = testCase.conn.query("sql","unittest_namespace","SELECT * FROM unittest_namespace.batch_table");
+ testCase.verifyEmpty(r);
+ end
+
+ function testSyntaxError(testCase)
+ testCase.verifyError(@() testCase.conn.query("sql","unittest_namespace","SELEC WRONG FROM nowhere"),?MException);
+ end
+
+
+ function testQueryWithSpaces(testCase)
+ % Insert with leading spaces
+ testCase.conn.query("sql","unittest_namespace", ...
+ " INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.conn.query("sql","unittest_namespace", ...
+ " INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+
+ r = testCase.conn.query("sql","unittest_namespace", ...
+ "SELECT id,name FROM unittest_namespace.unittest_table ORDER BY id");
+
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'id','name'});
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,'Alice'});
+ testCase.verifyEqual(table2cell(r(2,:)),{2,'Bob'});
+ end
+
+ function testConnectionFailure(testCase)
+ testCase.verifyError(@() ...
+ polypheny.Polypheny("localhost",9999,"pa","").query("sql","unittest_namespace","SELECT 1"), ...
+ ?MException);
+ end
+
+ function testCommitFailureRollback(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "Intentional nonsense to produce a failure"
+ };
+ testCase.verifyError(@() ...
+ testCase.conn.queryBatch("sql","unittest_namespace",queries),?MException);
+
+ r = testCase.conn.query("sql","unittest_namespace", ...
+ "SELECT * FROM unittest_namespace.batch_table");
+ testCase.verifyEmpty(r);
+ end
+
+ end
+
+end
\ No newline at end of file
diff --git a/README.md b/app/bin/main/README.md
similarity index 100%
rename from README.md
rename to app/bin/main/README.md
diff --git a/app/bin/main/startup.m b/app/bin/main/startup.m
new file mode 100644
index 0000000..c4dad7b
--- /dev/null
+++ b/app/bin/main/startup.m
@@ -0,0 +1,33 @@
+function startup
+ % Get root folder of the toolbox
+ root = fileparts(mfilename('fullpath'));
+
+ % Paths to JARs
+ jarPaths = { ...
+ fullfile(root, 'jar', 'polypheny-all.jar'), ...
+ fullfile(root, 'libs', 'polypheny-jdbc-driver-2.3.jar') ...
+ };
+
+ % Add JARs if not already on classpath
+ for i = 1:numel(jarPaths)
+ if ~any(strcmp(jarPaths{i}, javaclasspath('-all')))
+ javaaddpath(jarPaths{i});
+ end
+ end
+
+ % Try to register the JDBC driver dynamically
+ try
+ %java.lang.Class.forName('org.polypheny.jdbc.PolyphenyDriver');
+ driver = javaObject('org.polypheny.jdbc.PolyphenyDriver');
+ java.sql.DriverManager.registerDriver(driver);
+ catch e
+ warning('Could not register Polypheny JDBC driver dynamically: %s', char(e.message));
+ end
+
+ % Add MATLAB namespace folder (+polypheny)
+ if exist(fullfile(root, '+polypheny'), 'dir')
+ addpath(root);
+ end
+
+ disp('Polypheny connector initialized.');
+end
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..8fbf696
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'application'
+ id 'com.github.johnrengelman.shadow' version '8.1.1' //necessary plugin to create the .jar file for matlab later
+}
+
+application { mainClass = 'QuickTest' } // currently running QuickTest.java as application
+
+repositories { mavenCentral() }
+
+dependencies {
+ // SLF4J implementation to silence warnings
+ implementation 'org.slf4j:slf4j-simple:2.0.16'
+
+ // Polypheny JDBC driver
+ implementation files(rootProject.file('libs/polypheny-jdbc-driver-2.3.jar'))
+ testImplementation files(rootProject.file('libs/polypheny-jdbc-driver-2.3.jar'))
+
+ // Multimodel extension JAR (if you really need it)
+ runtimeOnly files(rootProject.file('libs/polypheny-jdbc-multimodel-2.3.jar'))
+ testRuntimeOnly files(rootProject.file('libs/polypheny-jdbc-multimodel-2.3.jar'))
+
+ // JUnit 5 API + engine
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
+}
+
+shadowJar {
+ archiveBaseName.set('polypheny') // final jar name base
+ archiveClassifier.set('all') // adds -all suffix
+ archiveVersion.set('') // => app/build/libs/polypheny-all.jar
+}
+
+java {
+ toolchain { languageVersion = JavaLanguageVersion.of(8) }
+}
+
+test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+ outputs.upToDateWhen { false }
+}
+
diff --git a/app/src/main/java/.gitignore b/app/src/main/java/.gitignore
new file mode 100644
index 0000000..b733afd
--- /dev/null
+++ b/app/src/main/java/.gitignore
@@ -0,0 +1,14 @@
+# Java build artifacts
+*.class
+*.log
+out/
+build/
+
+# VS Code settings (keep launch.json)
+
+# OS-specific files
+.DS_Store
+Thumbs.db
+
+# Ignore lib folder except .jar files
+lib/*
\ No newline at end of file
diff --git a/app/src/main/java/ClassTest.m b/app/src/main/java/ClassTest.m
new file mode 100644
index 0000000..47f32fc
--- /dev/null
+++ b/app/src/main/java/ClassTest.m
@@ -0,0 +1,9 @@
+clear all; clear classes
+%conn = javaObject('polyphenyconnector.PolyphenyConnection', 'localhost', int32(20590), 'pa', '');
+%exec = javaObject('polyphenyconnector.QueryExecutor', conn);
+
+%res = exec.execute('sql', 'SELECT 1 AS x');
+results = runtests('PolyphenyWrapperTest');
+disp(results)
+
+%disp(res);
diff --git a/app/src/main/java/LICENSE b/app/src/main/java/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/app/src/main/java/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/app/src/main/java/Main.java b/app/src/main/java/Main.java
new file mode 100644
index 0000000..f7bb97e
--- /dev/null
+++ b/app/src/main/java/Main.java
@@ -0,0 +1,22 @@
+import polyphenyconnector.PolyphenyConnection;
+import polyphenyconnector.QueryExecutor;
+
+public class Main {
+
+ public static void main( String[] args ) {
+ try {
+ String host = "localhost";
+ int port = 205090;
+ String user = "pa";
+ String pass = "";
+
+ PolyphenyConnection conn = new PolyphenyConnection( host, port, user, pass );
+ QueryExecutor executor = new QueryExecutor( conn );
+ executor.execute( "sql", "emps", "SELECT * FROM emps;" );
+ conn.close();
+ } catch ( Exception e ) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/app/src/main/java/Polypheny.m b/app/src/main/java/Polypheny.m
new file mode 100644
index 0000000..dfd0721
--- /dev/null
+++ b/app/src/main/java/Polypheny.m
@@ -0,0 +1,138 @@
+classdef Polypheny < handle
+% POLYPHENY MATLAB wrapper for the Polypheny Java connector. Wraps polyphenyconnector.PolyphenyConnection
+% and polyphenyconnector.QueryExecutor to run queries from MATLAB
+ properties ( Access = private )
+ polyConnection % Java PolyphenyConnection
+ queryExecutor % Java QueryExecutor
+
+ end
+
+ methods
+
+ function PolyWrapper = Polypheny( host, port, user, password )
+ % Polypheny( LANGUAGE, HOST, PORT, USER, PASSWORD ): Set up Java connection + executor
+ % LANGUAGE: The database language ( 'sql', 'mongo', 'cypher' )
+ % HOST: Database host ( e.g. 'localhost' )
+ % PORT: Database port ( integer )
+ % USER: Username
+ % PASSWORD: Password
+
+ % This makes sure that Matlab sees Java classes supplied by the .jar files in the Matlabtoolbox PolyphenyConnector.mtlbx
+ try
+ if ~polypheny.Polypheny.hasPolypheny( )
+ startup( );
+ end
+ PolyWrapper.polyConnection = javaObject( "polyphenyconnector.PolyphenyConnection",host, int32( port ), user, password );
+ PolyWrapper.queryExecutor = javaObject( "polyphenyconnector.QueryExecutor", PolyWrapper.polyConnection );
+
+ catch ME %Matlab Exception
+ disp( "Error: " + ME.message )
+ end
+
+ end
+
+
+
+ function matlab_result = query( PolyWrapper, language, namespace, queryStr )
+ % query( POLYWRAPPER, QUERYSTR ): Execute query via QueryExecutor.java
+ % POLYWRAPPER: The PolyWrapper Matlab object
+ % LANGUAGE: The language of the query string -> SQL, mongo, Cypher
+ % QUERYSTR: The queryStr set by the user
+ % @return matlab_result: The result of the query -> return type differs for SQL,Mongo and Cypher
+
+
+ try
+ java_result = PolyWrapper.queryExecutor.execute( string( language ), string( namespace ), queryStr );
+
+ switch lower( language )
+ case "sql"
+ if isempty( java_result )
+ matlab_result = [];
+ elseif isscalar( java_result )
+ matlab_result = java_result;
+ elseif isa( java_result,'java.lang.Object[]' ) && numel( java_result )==2
+ tmp = cell( java_result );
+ colNames = cell( tmp{1} );
+ data = cell( tmp{2} );
+ matlab_result = cell2table( data, 'VariableNames', colNames );
+ else
+ matlab_result = [];
+ end
+
+ case "mongo"
+ if isa( java_result, 'java.util.List' )
+ % Current driver behavior: always returns List of JSON docs
+ matlab_result = string(java_result);
+ elseif isnumeric( java_result )
+ % Not observed in current driver, but kept for forward compatibility
+ % (e.g. if Polypheny ever returns scalar counts directly)
+ matlab_result = java_result;
+ else
+ error( "Unexpected Mongo result type: %s", class( java_result ) );
+ end
+
+ case "cypher"
+ % TODO: integrate once Cypher executor is ready
+ error( "Cypher not supported yet." );
+
+ otherwise
+ error( "Unsupported language: %s", language );
+ end
+
+ catch ME
+ error( "Query execution failed: %s", ME.message );
+ end
+ end
+
+ function matlab_result = queryBatch( PolyWrapper, language, namespace, queryList )
+ % queryBatch( POLYWRAPPER, QUERYLIST ): Execute batch of non-SELECT statements
+ % QUERYLIST: A cell array of SQL strings ( INSERT, UPDATE, DELETE, etc. )
+ %
+ % Returns: int array with rows affected per statement
+
+ if ~iscell( queryList )
+ error( 'queryBatch expects a cell array of query strings' );
+ end
+
+ javaList = java.util.ArrayList();
+ for i = 1:numel( queryList )
+ javaList.add( string(queryList{i} ) );
+ end
+
+ switch lower(language)
+ case "sql"
+ java_result = PolyWrapper.queryExecutor.executeBatchSql( javaList );
+ %matlab_result = double(java_result(:))';
+ vals = double(java_result(:)); % convert Java int[] to MATLAB column vector
+ matlab_result = array2table(vals, 'VariableNames', {'RowsAffected'});
+
+ case "mongo"
+ java_result = PolyWrapper.queryExecutor.executeBatchMongo( string(namespace), javaList );
+ matlab_result = string( java_result ); % outer list
+
+ case "cypher"
+ error( "Batch execution for Cypher not yet implemented." );
+
+ otherwise
+ error( "Unsupported language: %s", language );
+ end
+
+ end
+
+ function close( PolyWrapper )
+ % close( POLYWRAPPER ): Close the Java connection
+ % POLYWRAPPER: The PolyWrapper Matlab object
+ PolyWrapper.polyConnection.close( );
+ end
+ end
+
+ methods ( Static )
+ function flag = hasPolypheny( )
+ % HASPOLYPHENY Returns true if Polypheny Java classes are available because the exist( 'polyphenyconnector.PolyphenyConnection','class' )
+ % returns 8 if Matlab sees the Java class and 0 otherwise.
+ flag = ( exist( 'polyphenyconnector.PolyphenyConnection','class' ) == 8 );
+ end
+
+ end
+
+end
diff --git a/app/src/main/java/PolyphenyWrapperTest.m b/app/src/main/java/PolyphenyWrapperTest.m
new file mode 100644
index 0000000..68b1909
--- /dev/null
+++ b/app/src/main/java/PolyphenyWrapperTest.m
@@ -0,0 +1,69 @@
+classdef PolyphenyWrapperTest < matlab.unittest.TestCase
+ properties
+ conn
+ end
+
+ methods (TestMethodSetup)
+ function setupConnection(testCase)
+ testCase.conn = polypheny.Polypheny('localhost', int32(20590), 'pa', '' );
+ end
+ end
+
+ methods (TestMethodTeardown)
+ function closeConnection(testCase)
+ testCase.conn.close();
+ end
+ end
+
+ methods (Test)
+ function testScalar(testCase)
+ r = testCase.conn.query( "sql" , "" , "SELECT 1 AS x");
+ testCase.verifyEqual(r, 1);
+ end
+
+ function testTable(testCase)
+ testCase.conn.query("sql" , "" , "DROP TABLE IF EXISTS wrapper_test");
+ testCase.conn.query("sql" , "" , "CREATE TABLE wrapper_test (id INTEGER PRIMARY KEY, name VARCHAR)");
+ testCase.conn.query("sql" , "" , "INSERT INTO wrapper_test VALUES (1,'Alice'),(2,'Bob')");
+
+ T = testCase.conn.query("sql" , "" , "SELECT * FROM wrapper_test ORDER BY id");
+
+ if istable(T)
+ % Expected: table output with column "name"
+ testCase.verifyEqual(T.name, {'Alice'; 'Bob'});
+ elseif iscell(T)
+ % Fallback: check the raw cell contents
+ testCase.verifyEqual(T(:,2), {'Alice','Bob'}');
+ else
+ testCase.verifyFail("Unexpected return type: " + class(T));
+ end
+ end
+
+ function testEmpty(testCase)
+ T = testCase.conn.query("sql" , "" , "SELECT * FROM wrapper_test WHERE id=999");
+ testCase.verifyEmpty(T);
+ end
+
+ function testBatchInsert(testCase)
+ % Prepare table
+ testCase.conn.query("sql" , "" , "DROP TABLE IF EXISTS batch_test");
+ testCase.conn.query("sql" , "" , "CREATE TABLE batch_test (id INTEGER PRIMARY KEY, name VARCHAR)");
+
+ % Batch insert 2 rows
+ queries = { ...
+ "INSERT INTO batch_test VALUES (1,'Alice')", ...
+ "INSERT INTO batch_test VALUES (2,'Bob')" ...
+ };
+ result = testCase.conn.queryBatch("sql" , "" , queries);
+
+ % Verify JDBC return codes
+ testCase.verifyEqual(result.RowsAffected', [1 1]);
+
+ % Verify table contents
+ T = testCase.conn.query("sql" , "" ,"SELECT id, name FROM batch_test ORDER BY id");
+ testCase.verifyEqual(T.id, [1; 2]);
+ testCase.verifyEqual(string(T.name), ["Alice"; "Bob"]);
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/app/src/main/java/PolyphenyWrapperTestMQL.m b/app/src/main/java/PolyphenyWrapperTestMQL.m
new file mode 100644
index 0000000..e992fc1
--- /dev/null
+++ b/app/src/main/java/PolyphenyWrapperTestMQL.m
@@ -0,0 +1,245 @@
+classdef PolyphenyWrapperTestMQL < matlab.unittest.TestCase
+ properties
+ conn
+ end
+
+ methods(TestClassSetup)
+ function setUpNamespaceAndCollection(testCase)
+ clc;
+ % open connection once
+ testCase.conn = polypheny.Polypheny("localhost",20590,"pa","");
+
+ % try create collection
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.createCollection("unittest_collection")');
+ catch
+ end
+ end
+ end
+
+ methods(TestClassTeardown)
+ function tearDownNamespaceAndCollection(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.drop()');
+ catch
+ end
+ testCase.conn.close();
+ end
+ end
+
+ methods(TestMethodSetup)
+ function clearCollection(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.deleteMany({})');
+ catch
+ end
+ end
+ end
+
+ methods(TestMethodTeardown)
+ function clearCollectionAfter(testCase)
+ try
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.deleteMany({})');
+ catch
+ end
+ end
+ end
+
+ methods(Test)
+
+ function testDeleteManyRemovesAllDocs(testCase)
+ % Drop & recreate collection
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.drop()');
+ testCase.conn.query("mongo","mongotest", 'db.createCollection("unittest_collection")');
+
+ % Insert three documents
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})');
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})');
+ testCase.conn.query("mongo","mongotest", 'db.unittest_collection.insertOne({"id":3,"name":"Ciri"})');
+
+ % Call deleteMany({})
+ ack = testCase.conn.query("mongo","mongotest", 'db.unittest_collection.deleteMany({})');
+ disp("Ack from deleteMany:");
+ disp(ack);
+
+ % Verify collection is empty
+ docs = testCase.conn.query("mongo","mongotest", 'db.unittest_collection.find({})');
+ docs = jsondecode(docs);
+ testCase.verifyEmpty(docs, "Collection should be empty after deleteMany({})");
+ end
+
+ function testInsertManyAndNestedDocument(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":14})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":20})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":24})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":30,"adress":{"Country":"Switzerland","Code":4051}})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({"age":{$gt:29}})');
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"age":30'));
+ end
+
+ function testBooleanField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"flag":true})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyTrue(contains(docs(1),'"flag":true'));
+ testCase.verifyClass(decoded.flag, 'logical'); % asserts that class(decoded.flag) == logical
+ end
+
+ function testIntegerAgeField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"age":42})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"age":42'));
+ end
+
+ function testStringField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"name":"Alice"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"name":"Alice"'));
+ end
+
+ function testLongField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"big":1111111111111111111})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"big":1111111111111111111'));
+ end
+
+ function testDoubleField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"pi":3.14159})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'"pi":3.14159'));
+ end
+
+ function testInsertAndQueryTwoDocsRawJson(testCase)
+ % Clean collection
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.drop()');
+ testCase.conn.query("mongo","mongotest",'db.createCollection("unittest_collection")');
+
+ % Insert two docs
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})');
+ testCase.conn.query("mongo","mongotest", ...
+ 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})');
+
+ % Query back
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp("Raw JSON:");
+ disp(docs);
+ decoded = jsondecode(docs);
+ disp(decoded)
+
+ % Assert raw JSON is exactly what we want
+ testCase.verifyTrue(contains(docs(1),'"name":"Alice"'));
+ testCase.verifyTrue(contains(docs(1),'"name":"Bob"'));
+ end
+
+
+ function testCountDocuments(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"name":"Bob"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.countDocuments({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs(1),'{"count":1}'));
+ end
+
+ function testArrayField(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"scores":[1,2,3]})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyTrue(contains(docs,'"scores":[1,2,3]'));
+ end
+
+ function testFindOnEmptyCollection(testCase)
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(docs,"[]");
+ end
+
+ function testInsertManyAndFindMultiple(testCase)
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"id":10,"name":"A"})');
+ testCase.conn.query("mongo","mongotest",'db.unittest_collection.insertOne({"id":11,"name":"B"})');
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyTrue(contains(docs,'"id":10'))
+ testCase.verifyTrue(contains(docs,'"name":"A"'))
+ testCase.verifyTrue(contains(docs,'"id":11'))
+ testCase.verifyTrue(contains(docs,'"name":"B"'))
+ end
+
+ function testBatchInsertAndFind(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"name":"Alice","age":25})', ...
+ 'db.unittest_collection.insertOne({"name":"Alice","age":20})', ...
+ 'db.unittest_collection.insertOne({"name":"Bob","age":30})' };
+ ignore = testCase.conn.queryBatch("mongo","mongotest",queries);
+ queries2 = { ...
+ 'db.unittest_collection.find({"name":"Alice"})', ...
+ 'db.unittest_collection.find({"name":"Alice","age":20})', ...
+ 'db.unittest_collection.find({"name":"Bob","age":30})' };
+ docs = testCase.conn.queryBatch("mongo","mongotest", queries2);
+ disp(docs)
+ decoded = jsondecode(docs);
+ disp(decoded)
+ testCase.verifyEqual(numel(decoded{1}), 2); % 2 docs in first query
+
+ % check names
+ names = {decoded{1}.name}; % cell of names
+ disp(names)
+ testCase.verifyEqual(string(names), ["Alice","Alice"]);
+
+ end
+
+
+ function testBatchMixedOps(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"name":"Charlie","active":true})', ...
+ 'db.unittest_collection.countDocuments({})' };
+ docs = testCase.conn.queryBatch("mongo","mongotest",queries);
+ testCase.verifyEqual(numel(docs),1);
+ decoded = jsondecode(docs)
+ varname = fieldnames(decoded{2})
+ disp(decoded{2}.count)
+ testCase.verifyTrue(decoded{2}.count==1);
+ end
+
+ function testSyntaxErrorThrows(testCase)
+ badQuery = 'db.unittest_collection.insertOne({"foo":123)'; % invalid JSON
+ testCase.verifyError(@() testCase.conn.query("mongo","mongotest",badQuery),?MException);
+ end
+
+ function testMultiStatementFails(testCase)
+ badMulti = [ ...
+ 'db.people.insertOne({"name":"Alice","age":20}); ' ...
+ 'db.people.insertOne({"name":"Bob","age":24}); ' ...
+ 'db.people.find({})' ];
+ testCase.verifyError(@() testCase.conn.query("mongo","mongotest",badMulti),?MException);
+ end
+
+ function testBatchRollback(testCase)
+ queries = { ...
+ 'db.unittest_collection.insertOne({"id":1,"name":"Alice"})', ...
+ 'db.unittest_collection.insertOne({"id":2,"name":"Bob"})', ...
+ 'db.unittest_collection.insertOne({"id":3,"name":"Janice"' }; % broken
+ testCase.verifyError(@() testCase.conn.queryBatch("mongo","mongotest",queries),?MException);
+ docs = testCase.conn.query("mongo","mongotest",'db.unittest_collection.find({})');
+ disp(docs)
+ testCase.verifyEqual(numel(docs),1);
+ testCase.verifyEqual(docs,"[]")
+ end
+
+ end
+end
diff --git a/app/src/main/java/PolyphenyWrapperTestSQL.m b/app/src/main/java/PolyphenyWrapperTestSQL.m
new file mode 100644
index 0000000..0b25e6b
--- /dev/null
+++ b/app/src/main/java/PolyphenyWrapperTestSQL.m
@@ -0,0 +1,220 @@
+classdef PolyphenyWrapperTestSQL < matlab.unittest.TestCase
+ properties
+ conn % polypheny.Polypheny wrapper
+ end
+
+ methods(TestClassSetup)
+ function setUpNamespaceAndTable(testCase)
+ clc;
+ % Open connection once for all tests
+ testCase.conn = polypheny.Polypheny("localhost",20590,"pa","");
+
+ % Drop leftovers if they exist
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP NAMESPACE IF EXISTS unittest_namespace");
+ catch
+ end
+
+ % Create namespace + table for execute()
+ testCase.conn.query("sql","", ...
+ "CREATE NAMESPACE unittest_namespace");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "CREATE TABLE unittest_namespace.unittest_table (id INT NOT NULL, name VARCHAR(100), PRIMARY KEY(id))");
+
+ % Drop and recreate batch_table
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.batch_table");
+ catch
+ end
+ testCase.conn.query("sql","unittest_namespace", ...
+ "CREATE TABLE unittest_namespace.batch_table (" + ...
+ "emp_id INT NOT NULL, " + ...
+ "name VARCHAR(100), " + ...
+ "gender VARCHAR(10), " + ...
+ "birthday DATE, " + ...
+ "employee_id INT, " + ...
+ "PRIMARY KEY(emp_id))");
+ end
+ end
+
+ methods(TestClassTeardown)
+ function tearDownNamespaceAndTable(testCase)
+ % Cleanup after all tests
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DROP TABLE IF EXISTS unittest_namespace.batch_table");
+ testCase.conn.query("sql","", ...
+ "DROP NAMESPACE IF EXISTS unittest_namespace");
+ catch
+ end
+ testCase.conn.close();
+ end
+ end
+
+ methods(TestMethodSetup)
+ function clearTables(testCase)
+ % Clear before each test
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.batch_table");
+ catch
+ end
+ end
+ end
+
+ methods(TestMethodTeardown)
+ function clearTablesAfter(testCase)
+ % Clear again after each test
+ try
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.unittest_table");
+ testCase.conn.query("sql","unittest_namespace", ...
+ "DELETE FROM unittest_namespace.batch_table");
+ catch
+ end
+ end
+ end
+
+ methods(Test)
+ function testScalarLiteral(testCase)
+ r = testCase.conn.query("sql","","SELECT 42 AS answer");
+ testCase.verifyEqual(r,42);
+ end
+
+ function testEmptyLiteral(testCase)
+ r = testCase.conn.query("sql","","SELECT * FROM (SELECT 1) t WHERE 1=0");
+ testCase.verifyEmpty(r);
+ end
+
+ function testTableLiteral(testCase)
+ r = testCase.conn.query("sql","unittest_namespace","SELECT 1 AS a, 2 AS b UNION ALL SELECT 3,4");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'a','b'});
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,2});
+ testCase.verifyEqual(table2cell(r(2,:)),{3,4});
+ end
+
+ function testInsert(testCase)
+ r = testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.verifyEqual(r,1);
+ end
+
+ function testInsertAndSelect(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id,name FROM unittest_namespace.unittest_table");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'id','name'});
+ testCase.verifyEqual(height(r),1);
+ testCase.verifyEqual(table2cell(r),{1,'Alice'});
+ end
+
+ function testScalarFromTable(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Carol')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id FROM unittest_namespace.unittest_table WHERE name='Carol'");
+ testCase.verifyEqual(r,2);
+ end
+
+ function testInsertAndSelectMultipleRows(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT id,name FROM unittest_namespace.unittest_table ORDER BY id");
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,'Alice'});
+ testCase.verifyEqual(table2cell(r(2,:)),{2,'Bob'});
+ end
+
+ function testDeleteFromTable(testCase)
+ testCase.conn.query("sql","unittest_namespace","INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+ testCase.conn.query("sql","unittest_namespace","DELETE FROM unittest_namespace.unittest_table");
+ r = testCase.conn.query("sql","unittest_namespace","SELECT * FROM unittest_namespace.unittest_table");
+ testCase.verifyEmpty(r);
+ end
+
+ function testBatchInsertEmployees(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (2,'Bob','M',DATE '1989-05-12',1002)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (3,'Jane','F',DATE '1992-07-23',1003)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (4,'Tim','M',DATE '1991-03-03',1004)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (5,'Alex','M',DATE '1994-11-11',1005)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (6,'Mason','M',DATE '1988-04-22',1006)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (7,'Rena','F',DATE '1995-06-17',1007)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (8,'Christopher','M',DATE '1987-08-09',1008)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (9,'Lexi','F',DATE '1996-09-30',1009)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (10,'Baen','M',DATE '1990-10-05',1010)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (11,'Ricardo','M',DATE '1986-12-12',1011)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (12,'Tim','M',DATE '1993-02-02',1012)"
+ "INSERT INTO unittest_namespace.batch_table VALUES (13,'Beya','F',DATE '1994-05-25',1013)"
+ };
+ counts = testCase.conn.queryBatch("sql","unittest_namespace",queries);
+ testCase.verifyEqual(height(counts),13);
+ disp(counts)
+ testCase.verifyTrue(all(counts.RowsAffected == 1));
+ r = testCase.conn.query("sql","unittest_namespace","SELECT COUNT(*) FROM unittest_namespace.batch_table");
+ testCase.verifyEqual(r,13);
+ end
+
+ function testBatchRollbackOnFailure(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "BROKEN QUERY"
+ };
+ testCase.verifyError(@() testCase.conn.queryBatch("sql","unittest_namespace",queries),?MException);
+ r = testCase.conn.query("sql","unittest_namespace","SELECT * FROM unittest_namespace.batch_table");
+ testCase.verifyEmpty(r);
+ end
+
+ function testSyntaxError(testCase)
+ testCase.verifyError(@() testCase.conn.query("sql","unittest_namespace","SELEC WRONG FROM nowhere"),?MException);
+ end
+
+
+ function testQueryWithSpaces(testCase)
+ % Insert with leading spaces
+ testCase.conn.query("sql","unittest_namespace", ...
+ " INSERT INTO unittest_namespace.unittest_table VALUES (1,'Alice')");
+ testCase.conn.query("sql","unittest_namespace", ...
+ " INSERT INTO unittest_namespace.unittest_table VALUES (2,'Bob')");
+
+ r = testCase.conn.query("sql","unittest_namespace", ...
+ "SELECT id,name FROM unittest_namespace.unittest_table ORDER BY id");
+
+ testCase.verifyTrue(istable(r));
+ testCase.verifyEqual(r.Properties.VariableNames,{'id','name'});
+ testCase.verifyEqual(height(r),2);
+ testCase.verifyEqual(table2cell(r(1,:)),{1,'Alice'});
+ testCase.verifyEqual(table2cell(r(2,:)),{2,'Bob'});
+ end
+
+ function testConnectionFailure(testCase)
+ testCase.verifyError(@() ...
+ polypheny.Polypheny("localhost",9999,"pa","").query("sql","unittest_namespace","SELECT 1"), ...
+ ?MException);
+ end
+
+ function testCommitFailureRollback(testCase)
+ queries = {
+ "INSERT INTO unittest_namespace.batch_table VALUES (1,'Alice','F',DATE '1990-01-15',1001)"
+ "Intentional nonsense to produce a failure"
+ };
+ testCase.verifyError(@() ...
+ testCase.conn.queryBatch("sql","unittest_namespace",queries),?MException);
+
+ r = testCase.conn.query("sql","unittest_namespace", ...
+ "SELECT * FROM unittest_namespace.batch_table");
+ testCase.verifyEmpty(r);
+ end
+
+ end
+
+end
\ No newline at end of file
diff --git a/app/src/main/java/README.md b/app/src/main/java/README.md
new file mode 100644
index 0000000..5a16b89
--- /dev/null
+++ b/app/src/main/java/README.md
@@ -0,0 +1,2 @@
+# Matlab-Connector
+Addon for Matlab to connect and query a Polypheny database.
diff --git a/app/src/main/java/org/example/App.java b/app/src/main/java/org/example/App.java
new file mode 100644
index 0000000..e7e1af9
--- /dev/null
+++ b/app/src/main/java/org/example/App.java
@@ -0,0 +1,14 @@
+/*
+ * This source file was generated by the Gradle 'init' task
+ */
+package org.example;
+
+public class App {
+ public String getGreeting() {
+ return "Hello World!";
+ }
+
+ public static void main(String[] args) {
+ System.out.println(new App().getGreeting());
+ }
+}
diff --git a/app/src/main/java/polyphenyconnector/PolyphenyConnection.java b/app/src/main/java/polyphenyconnector/PolyphenyConnection.java
new file mode 100644
index 0000000..b2edf6d
--- /dev/null
+++ b/app/src/main/java/polyphenyconnector/PolyphenyConnection.java
@@ -0,0 +1,147 @@
+package polyphenyconnector;
+
+import java.sql.*;
+
+public class PolyphenyConnection {
+
+ private Connection connection;
+ private final String host, url, username, password;
+ private final int port;
+
+
+ /**
+ * @Description
+ * - Constructor supporting lazy-open: Stores logins; connects on first use to protect server resources.
+ *
+ * @param host: the host that should be used for the connection
+ * @param port: the port that should be used for the connection
+ * @param username: username to access the database with
+ * @param password: password to the corresponding username
+ *
+ **/
+ public PolyphenyConnection( String host, int port, String username, String password ) {
+ this.host = host;
+ this.port = port;
+ this.url = "jdbc:polypheny://" + host + ":" + port;
+ this.username = username;
+ this.password = password;
+ this.connection = null; // The connection is established later on when needed. Lazy open
+ // prevents accidental resource leaks induced by user
+ }
+
+
+ /**
+ * @Description
+ * - Opens the server connection to Polypheny if needed (reuse otherwise). Checking the
+ * if-clause in java is a lot faster, than iterative opening and closing of the connection after every
+ * use for large numbers of queries, as it eliminates the ~10ms matlab-java crossover that opening and
+ * closing a connection from matlab would create. For 1M queries that avoids 1M*10ms = ~10 000sec=2.8 hrs
+ * of overhead.
+ *
+ **/
+ public void openIfNeeded() {
+ if ( connection == null ) {
+ try {
+ connection = DriverManager.getConnection( url, username, password );
+ } catch ( SQLException e ) {
+ throw new RuntimeException( "Failed to open connection", e );
+ }
+ }
+ }
+
+
+ /**
+ * @Description
+ * - Getter function for the host
+ * @return host The host passed to the PolyphenyConnection object.
+ */
+ public String getHost() {
+ return host;
+ }
+
+
+ /**
+ * @Description
+ * - Getter function for the port
+ * @return port The port passed to the PolyphenyConnection object.
+ */
+ public int getPort() {
+ return port;
+ }
+
+
+ /**
+ * @Description
+ * - Closes connection if open
+ *
+ **/
+ public void close() {
+ try {
+ if ( connection != null && !connection.isClosed() ) {
+ connection.close();
+ }
+ } catch ( SQLException e ) {
+ throw new RuntimeException( "Failed to close connection: " + e.getMessage() );
+ } finally {
+ connection = null;
+ }
+ }
+
+
+ /**
+ * @Description
+ * - Getter function for the connection variable of PolyphenyConnection
+ *
+ * @return
+ * - Connection connection variable of the PolyphenyConnection class
+ **/
+ public Connection getConnection() {
+ return this.connection;
+ }
+
+
+ /**
+ * @Description
+ * - Begins Database transaction. This is necessary to expose here because we need it to control flow in
+ * Batch queries handled in the QueryExecutor class later.
+ *
+ * @throws SQLException
+ */
+ public void beginTransaction() throws SQLException {
+ openIfNeeded();
+ connection.setAutoCommit( false );
+ }
+
+
+ /**
+ * @Description
+ * - Commits Database transaction. This is necessary to expose here because we need it to control flow in
+ * Batch queries handled in the QueryExecutor class later.
+ *
+ * @throws SQLException
+ */
+ public void commitTransaction() throws SQLException {
+ connection.commit();
+ connection.setAutoCommit( true );
+
+ }
+
+
+ /**
+ * @Description
+ * - Rolls back Database transaction. This is necessary to expose here because we need it to control flow in
+ * Batch queries handled in the QueryExecutor class later.
+ *
+ * @throws SQLException
+ */
+ public void rollbackTransaction() throws SQLException {
+ connection.rollback();
+ connection.setAutoCommit( true );
+ }
+
+
+ public void setAutoCommit( boolean AutoCommitMode ) throws SQLException {
+ connection.setAutoCommit( AutoCommitMode );
+ }
+
+}
diff --git a/app/src/main/java/polyphenyconnector/QueryExecutor.java b/app/src/main/java/polyphenyconnector/QueryExecutor.java
new file mode 100644
index 0000000..5a4660e
--- /dev/null
+++ b/app/src/main/java/polyphenyconnector/QueryExecutor.java
@@ -0,0 +1,447 @@
+package polyphenyconnector;
+
+import java.sql.*;
+import java.util.*;
+
+import org.polypheny.jdbc.PolyConnection;
+import org.polypheny.jdbc.multimodel.*;
+import org.polypheny.jdbc.types.*;
+
+public class QueryExecutor {
+
+ private PolyphenyConnection polyconnection;
+
+
+ /**
+ * @Description
+ * - Constructor
+ *
+ * @param polyconnection: PolyphenyConnection object that holds the connection
+ * details to the Database. It's used to execute queries
+ **/
+ public QueryExecutor( PolyphenyConnection polyconnection ) {
+ this.polyconnection = polyconnection;
+ }
+
+
+ /**
+ * @Description
+ * - Executes the query depending on the language given by the user
+ *
+ * @param language: The database language that is used (e.g. SQL, Mongo,Cypher)
+ * @param namespace: The namespace in the query string. For SQL this argument as no effect as we use JDBC's
+ * executeQuery(...)/executeUpdate(...). For MQL this argument will be passed to JDBC's execute(...) function. For further
+ * information consult Polpyheny's web documentation.
+ * @param query: The query-text to be executed (e.g. FROM emps SELECT *)
+ *
+ * @return: ResultToMatlab(rs) which is a Matlab compatible object that is cast to the Matlab user.
+ **/
+ public Object execute( String language, String namespace, String query ) {
+ polyconnection.openIfNeeded();
+
+ switch ( language.toLowerCase() ) {
+ default:
+ throw new UnsupportedOperationException( "Unsupported language: " + language );
+
+ case "sql":
+ try ( Statement stmt = polyconnection.getConnection().createStatement() ) {
+
+ String first = query.trim().toUpperCase();
+
+ // SELECT statements
+ if ( first.startsWith( "SELECT" ) ) {
+ try ( ResultSet rs = stmt.executeQuery( query ) ) {
+ return SQLResultToMatlab( rs );
+ }
+
+ // INSERT, UPDATE, DELETE, CREATE, DROP, ... statements
+ } else {
+ int rs = stmt.executeUpdate( query );
+ return rs;
+
+ }
+
+ } catch ( SQLException e ) {
+ throw translateException( e );
+ } catch ( Exception e ) {
+ throw new RuntimeException( "SQL execution failed: " + e.getMessage(), e );
+ }
+
+ case "mongo":
+ try {
+
+ // Get the connection variable from the PolyphenyConnection.java class using the getter
+ Connection connection = polyconnection.getConnection();
+
+ // Unwrap Connection connection to the JDBC Driver-supplied PolyConnection polyConnection
+ PolyConnection polyConnection = connection.unwrap( PolyConnection.class );
+
+ // Create a PolyStatement object to call .execute(...) method of the JDBC-Driver on
+ PolyStatement polyStatement = polyConnection.createPolyStatement();
+
+ // Call the execute(...) function on the polyStatement
+ Result result = polyStatement.execute( namespace, language, query );
+
+ switch ( result.getResultType() ) {
+
+ case DOCUMENT:
+ // Unwrapping according to → https://docs.polypheny.com/en/latest/drivers/jdbc/extensions/result
+ DocumentResult documentResult = result.unwrap( DocumentResult.class );
+ return DocumentToMatlab( documentResult );
+
+ // This case was never used in any of the JUnit Tests, as every Mongo Query currently seems to be wrapped as
+ // PolyDocument by the JDBC Driver. The case was still left in as security based on the official documentation:
+ // → https://docs.polypheny.com/en/latest/drivers/jdbc/extensions/result
+ case SCALAR:
+ ScalarResult scalarResult = result.unwrap( ScalarResult.class );
+ long scalar = scalarResult.getScalar();
+ return scalar;
+
+ default:
+ throw new UnsupportedOperationException( "Unhandled result type: " + result.getResultType() );
+ }
+ } catch ( SQLException e ) {
+ throw translateException( e );
+ } catch ( Exception e ) {
+ throw new RuntimeException( "Mongo execution failed: " + e.getMessage(), e );
+ }
+ case "cypher":
+ throw new UnsupportedOperationException( "Cypher execution not yet implemented." );
+ }
+
+ }
+
+
+ /**
+ * @Description
+ * This function is capable of executing a List of non-SELECT SQL statements in one single Matlab-Java crossing.
+ * All SQL statements except SELECT are supported. For further information consult the Polypheny JDBC Driver documentation
+ * → https://docs.polypheny.com/en/latest/drivers/jdbc/relational/statement
+ *
+ * @param queries The list of SQL query strings to be executed.
+ * @return List result A list of integers, where the i-th entry will denote for the i-th query how many rows were touched, e.g.
+ * n: n rows were updated, 0: no rows were touched.
+ */
+ public int[] executeBatchSql( List queries ) {
+ polyconnection.openIfNeeded();
+ try {
+ polyconnection.beginTransaction();
+ try ( Statement stmt = polyconnection.getConnection().createStatement() ) {
+ for ( String query : queries ) {
+ String first = query.trim().toUpperCase();
+ if ( first.startsWith( "SELECT" ) ) {
+ throw new UnsupportedOperationException( "Batch execution does not support SELECT statements." );
+ }
+ stmt.addBatch( query );
+ }
+ int[] result = stmt.executeBatch();
+ polyconnection.commitTransaction();
+ return result; // return directly
+ } catch ( SQLException e ) {
+ try {
+ polyconnection.rollbackTransaction();
+ } catch ( Exception rollbackException ) {
+ // Propagate both the batch failure AND the rollback failure → User must be made
+ throw new RuntimeException( "SQL batch failed AND rollback failed: " + rollbackException.getMessage(), e );
+ }
+ throw translateException( e );
+ } catch ( Exception e ) {
+ try {
+ polyconnection.rollbackTransaction();
+ } catch ( Exception rollbackEx ) {
+ // Propagate both the batch failure AND the rollback failure → User must be made
+ throw new RuntimeException( "SQL batch failed AND rollback failed: " + rollbackEx.getMessage(), e );
+ }
+ throw new RuntimeException( "SQL batch execution failed. Transaction was rolled back: " + e.getMessage(), e );
+ }
+
+ } catch ( SQLException e ) {
+ throw new RuntimeException( "Failed to manage transaction", e );
+ }
+ }
+
+
+ /**
+ * @Description
+ * This function is capable of executing a List of Mongo statements in one single Matlab-Java crossing.
+ * Each query is executed individually via the execute(...) method. The result of each query will be a List
+ * containing the JSON-encoded documents or scalars (as JSON strings). All individual query results are then grouped
+ * into an outer List, which represents the batch result.
+ *
+ * @param namespace The Mongo namespace (e.g. database / collection context).
+ * @param queries The list of Mongo query strings to be executed.
+ * @return List> result An outer list with one entry per query. Each entry is a List containing
+ * the documents or scalar results (as JSON strings) returned by the respective query.
+ */
+ public List> executeBatchMongo( String namespace, List queries ) {
+ polyconnection.openIfNeeded();
+ List> results = new ArrayList<>();
+ try {
+ polyconnection.beginTransaction();
+
+ for ( String query : queries ) {
+ @SuppressWarnings("unchecked") List result = (List) execute( "mongo", namespace, query );
+ results.add( result );
+ }
+
+ polyconnection.commitTransaction(); // commit if all succeeded
+ return results;
+
+ } catch ( Exception e ) {
+ try {
+ polyconnection.rollbackTransaction(); // rollback if anything failed
+ } catch ( Exception rollbackEx ) {
+ throw new RuntimeException( "Rollback failed after batch error", rollbackEx );
+ }
+ throw new RuntimeException( "Batch execution failed", e );
+ }
+ }
+
+
+ /**
+ * @Description
+ * - Casts the result of SQL queries to MatlabObjects
+ *
+ * @param rs: The result object of the query of type ResultSet
+ *
+ * @return: Result from the query which is either null/scalar/table
+ **/
+ public Object SQLResultToMatlab( ResultSet rs ) throws Exception {
+
+ ResultSetMetaData meta = rs.getMetaData();
+ int colCount = meta.getColumnCount();
+ Object[][] ResultArray;
+
+ // ─────────────────────────────
+ // Case 1: Empty Result
+ // ─────────────────────────────
+ if ( !rs.next() ) {
+ //System.out.println( "Empty result set." );
+ return null;
+ }
+
+ // ─────────────────────────────
+ // Case 2: Scalar Result
+ // ─────────────────────────────
+ if ( colCount == 1 && rs.isLast() ) {
+ //System.out.println( "Scalar result set." );
+ Object scalar = rs.getObject( 1 );
+ return scalar;
+ }
+
+ // ─────────────────────────────
+ // Case 3: Tabular Result (≥1 column, ≥1 row)
+ // ─────────────────────────────
+ String[] colNames = new String[colCount]; // get the column names to name the columns later
+
+ for ( int i = 1; i <= colCount; i++ ) {
+ colNames[i - 1] = meta.getColumnName( i ); // assign the column names to the array
+ }
+
+ List