diff --git a/+dj/Relvar.m b/+dj/Relvar.m index 802845ba..44ecda84 100755 --- a/+dj/Relvar.m +++ b/+dj/Relvar.m @@ -352,14 +352,25 @@ function update(self, attrname, value) switch true case isNull + assert(header.attributes(ix).isnullable, ... + 'attribute `%s` is not nullable.', attrname) valueStr = 'NULL'; value = {}; case header.attributes(ix).isString assert(dj.lib.isString(value), 'Value must be a string') - valueStr = '"{S}"'; - value = {char(value)}; + if isempty(value) + assert(header.attributes(ix).isnullable, ... + 'attribute `%s` is not nullable.', attrname) + valueStr = 'NULL'; + value = {}; + else + valueStr = '"{S}"'; + value = {char(value)}; + end case header.attributes(ix).isBlob if isempty(value) && header.attributes(ix).isnullable + assert(header.attributes(ix).isnullable, ... + 'attribute `%s` is not nullable.', attrname) valueStr = 'NULL'; value = {}; else diff --git a/+tests/+lib/compareVersions.m b/+tests/+lib/compareVersions.m new file mode 100644 index 00000000..46d7b27d --- /dev/null +++ b/+tests/+lib/compareVersions.m @@ -0,0 +1,100 @@ +function res = compareVersions(verArray, verComp) + % compareVersions - Semantic version comparison (greater than or equal) + % + % This function evaluates if an array of semantic versions is greater than + % or equal to a reference version. + % + % DISTRIBUTION: + % GitHub: https://github.com/guzman-raphael/compareVersions + % FileExchange: https://www.mathworks.com/matlabcentral/fileexchange/71849-compareversions + % + % res = compareVersions(verArray, verComp) + % INPUT: + % verArray: Cell array with the following conditions: + % - be of length >= 1, + % - contain only string elements, and + % - each element must be of length >= 1. + % verComp: String or Char array that verArray will compare against for + % greater than evaluation. Must be: + % - be of length >= 1, and + % - a string. + % OUTPUT: + % res: Logical array that identifies if each cell element in verArray + % is greater than or equal to verComp. + % TESTS: + % Tests included for reference. From root package directory, + % use command: runtests + % + % EXAMPLES: + % output = compareVersions({'3.2.4beta','9.5.2.1','8.0'}, '8.0.0'); %logical([0 1 1]) + % + % NOTES: + % Tests included for reference. From root package directory, + % use command: runtests + % + % Tested: Matlab 9.5.0.944444 (R2018b) Linux + % Author: Raphael Guzman, DataJoint + % + % $License: MIT (use/copy/change/redistribute on own risk) $ + % $File: compareVersions.m $ + % History: + % 001: 2019-06-12 11:00, First version. + % + % OPEN BUGS: + % - None + res_n = length(verArray); + if ~res_n || max(cellfun(@(c) ~ischar(c) && ... + ~isstring(c),verArray)) > 0 || min(cellfun('length',verArray)) == 0 + msg = { + 'compareVersions:Error:CellArray' + 'Cell array to verify must:' + '- be of length >= 1,' + '- contain only string elements, and' + '- each element must be of length >= 1.' + }; + error('compareVersions:Error:CellArray', sprintf('%s\n',msg{:})); + end + if ~ischar(verComp) && ~isstring(verComp) || length(verComp) == 0 + msg = { + 'compareVersions:Error:VersionRef' + 'Version reference must:' + '- be of length >= 1, and' + '- a string.' + }; + error('compareVersions:Error:VersionRef', sprintf('%s\n',msg{:})); + end + res = false(1, res_n); + for i = 1:res_n + shortVer = strsplit(verArray{i}, '.'); + shortVer = cellfun(@(x) str2double(regexp(x,'\d*','Match')), shortVer(1,:)); + longVer = strsplit(verComp, '.'); + longVer = cellfun(@(x) str2double(regexp(x,'\d*','Match')), longVer(1,:)); + shortVer_p = true; + longVer_p = false; + shortVer_s = length(shortVer); + longVer_s = length(longVer); + + if shortVer_s > longVer_s + [longVer shortVer] = deal(shortVer,longVer); + [longVer_s shortVer_s] = deal(shortVer_s,longVer_s); + [longVer_p shortVer_p] = deal(shortVer_p,longVer_p); + end + + shortVer = [shortVer zeros(1,longVer_s - shortVer_s)]; + diff = shortVer - longVer; + match = diff ~= 0; + + if ~match + res(i) = true; + else + pos = 1:longVer_s; + pos = pos(match); + val = diff(pos(1)); + if val > 0 + res(i) = shortVer_p; + elseif val < 0 + res(i) = longVer_p; + end + end + end +end \ No newline at end of file diff --git a/+tests/Main.m b/+tests/Main.m index e7986e8e..4f14f0a7 100644 --- a/+tests/Main.m +++ b/+tests/Main.m @@ -1,102 +1,5 @@ classdef Main < ... tests.TestConnection & ... + tests.TestRelationalOperand & ... tests.TestTls - - properties (Constant) - CONN_INFO_ROOT = struct(... - 'host', getenv('DJ_HOST'), ... - 'user', getenv('DJ_USER'), ... - 'password', getenv('DJ_PASS')); - CONN_INFO = struct(... - 'host', getenv('DJ_TEST_HOST'), ... - 'user', getenv('DJ_TEST_USER'), ... - 'password', getenv('DJ_TEST_PASSWORD')); - end - - methods (TestClassSetup) - function init(testCase) - disp('---------------INIT---------------'); - clear functions; - testCase.addTeardown(@testCase.dispose); - - curr_conn = dj.conn(testCase.CONN_INFO_ROOT.host, ... - testCase.CONN_INFO_ROOT.user, testCase.CONN_INFO_ROOT.password,'',true); - - ver = curr_conn.query('select @@version as version').version; - if dj.lib.compareVersions(ver,'5.8') - cmd = {... - 'CREATE USER IF NOT EXISTS ''datajoint''@''%%'' ' - 'IDENTIFIED BY ''datajoint'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'GRANT ALL PRIVILEGES ON `djtest%%`.* TO ''datajoint''@''%%'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'CREATE USER IF NOT EXISTS ''djview''@''%%'' ' - 'IDENTIFIED BY ''djview'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'GRANT SELECT ON `djtest%%`.* TO ''djview''@''%%'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'CREATE USER IF NOT EXISTS ''djssl''@''%%'' ' - 'IDENTIFIED BY ''djssl'' ' - 'REQUIRE SSL;' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'GRANT SELECT ON `djtest%%`.* TO ''djssl''@''%%'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - else - cmd = {... - 'GRANT ALL PRIVILEGES ON `djtest%%`.* TO ''datajoint''@''%%'' ' - 'IDENTIFIED BY ''datajoint'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'GRANT SELECT ON `djtest%%`.* TO ''djview''@''%%'' ' - 'IDENTIFIED BY ''djview'';' - }; - curr_conn.query(sprintf('%s',cmd{:})); - - cmd = {... - 'GRANT SELECT ON `djtest%%`.* TO ''djssl''@''%%'' ' - 'IDENTIFIED BY ''djssl'' ' - 'REQUIRE SSL;' - }; - curr_conn.query(sprintf('%s',cmd{:})); - end - end - end - - methods (Static) - function dispose() - disp('---------------DISP---------------'); - warning('off','MATLAB:RMDIR:RemovedFromPath'); - - curr_conn = dj.conn(tests.Main.CONN_INFO_ROOT.host, ... - tests.Main.CONN_INFO_ROOT.user, tests.Main.CONN_INFO_ROOT.password, '',true); - - cmd = {... - 'DROP USER ''datajoint''@''%%'';' - 'DROP USER ''djview''@''%%'';' - 'DROP USER ''djssl''@''%%'';' - }; - res = curr_conn.query(sprintf('%s',cmd{:})); - curr_conn.delete; - - warning('on','MATLAB:RMDIR:RemovedFromPath'); - end - end -end +end \ No newline at end of file diff --git a/+tests/Prep.m b/+tests/Prep.m new file mode 100644 index 00000000..47395249 --- /dev/null +++ b/+tests/Prep.m @@ -0,0 +1,135 @@ +classdef Prep < matlab.unittest.TestCase + % Setup and teardown for tests. + properties (Constant) + CONN_INFO_ROOT = struct(... + 'host', getenv('DJ_HOST'), ... + 'user', getenv('DJ_USER'), ... + 'password', getenv('DJ_PASS')); + CONN_INFO = struct(... + 'host', getenv('DJ_TEST_HOST'), ... + 'user', getenv('DJ_TEST_USER'), ... + 'password', getenv('DJ_TEST_PASSWORD')); + S3_CONN_INFO = struct(... + 'endpoint', getenv('S3_ENDPOINT'), ... + 'access_key', getenv('S3_ACCESS_KEY'), ... + 'secret_key', getenv('S3_SECRET_KEY'), ... + 'bucket', getenv('S3_BUCKET')); + PREFIX = 'djtest'; + end + properties + test_root; + end + methods + function obj = Prep() + % Initialize test_root + test_pkg_details = what('tests'); + [test_root, ~, ~] = fileparts(test_pkg_details.path); + obj.test_root = [test_root '/+tests']; + end + end + methods (TestClassSetup) + function init(testCase) + disp('---------------INIT---------------'); + clear functions; + addpath([testCase.test_root '/test_schemas']); + + curr_conn = dj.conn(testCase.CONN_INFO_ROOT.host, ... + testCase.CONN_INFO_ROOT.user, testCase.CONN_INFO_ROOT.password,'',true); + % create test users + ver = curr_conn.query('select @@version as version').version; + if tests.lib.compareVersions(ver,'5.8') + cmd = {... + 'CREATE USER IF NOT EXISTS ''datajoint''@''%%'' ' + 'IDENTIFIED BY ''datajoint'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'GRANT ALL PRIVILEGES ON `djtest%%`.* TO ''datajoint''@''%%'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'CREATE USER IF NOT EXISTS ''djview''@''%%'' ' + 'IDENTIFIED BY ''djview'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'GRANT SELECT ON `djtest%%`.* TO ''djview''@''%%'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'CREATE USER IF NOT EXISTS ''djssl''@''%%'' ' + 'IDENTIFIED BY ''djssl'' ' + 'REQUIRE SSL;' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'GRANT SELECT ON `djtest%%`.* TO ''djssl''@''%%'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + else + cmd = {... + 'GRANT ALL PRIVILEGES ON `djtest%%`.* TO ''datajoint''@''%%'' ' + 'IDENTIFIED BY ''datajoint'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'GRANT SELECT ON `djtest%%`.* TO ''djview''@''%%'' ' + 'IDENTIFIED BY ''djview'';' + }; + curr_conn.query(sprintf('%s',cmd{:})); + + cmd = {... + 'GRANT SELECT ON `djtest%%`.* TO ''djssl''@''%%'' ' + 'IDENTIFIED BY ''djssl'' ' + 'REQUIRE SSL;' + }; + curr_conn.query(sprintf('%s',cmd{:})); + end + end + end + methods (TestClassTeardown) + function dispose(testCase) + disp('---------------DISP---------------'); + warning('off','MATLAB:RMDIR:RemovedFromPath'); + + curr_conn = dj.conn(testCase.CONN_INFO_ROOT.host, ... + testCase.CONN_INFO_ROOT.user, testCase.CONN_INFO_ROOT.password, '',true); + + % remove databases + curr_conn.query('SET FOREIGN_KEY_CHECKS=0;'); + res = curr_conn.query(['SHOW DATABASES LIKE "' testCase.PREFIX '_%";']); + for i = 1:length(res.(['Database (' testCase.PREFIX '_%)'])) + curr_conn.query(['DROP DATABASE ' ... + res.(['Database (' testCase.PREFIX '_%)']){i} ';']); + end + curr_conn.query('SET FOREIGN_KEY_CHECKS=1;'); + + % remove users + cmd = {... + 'DROP USER ''datajoint''@''%%'';' + 'DROP USER ''djview''@''%%'';' + 'DROP USER ''djssl''@''%%'';' + }; + res = curr_conn.query(sprintf('%s',cmd{:})); + curr_conn.delete; + + % Remove getSchemas to ensure they are created by tests. + files = dir([testCase.test_root '/test_schemas']); + dirFlags = [files.isdir] & ~strcmp({files.name},'.') & ~strcmp({files.name},'..'); + subFolders = files(dirFlags); + for k = 1 : length(subFolders) + delete([testCase.test_root '/test_schemas/' subFolders(k).name ... + '/getSchema.m']); + % delete(['test_schemas/+University/getSchema.m']) + end + rmpath([testCase.test_root '/test_schemas']); + warning('on','MATLAB:RMDIR:RemovedFromPath'); + end + end +end \ No newline at end of file diff --git a/+tests/TestConnection.m b/+tests/TestConnection.m index d57d901b..92b113a3 100644 --- a/+tests/TestConnection.m +++ b/+tests/TestConnection.m @@ -1,7 +1,7 @@ -classdef TestConnection < matlab.unittest.TestCase +classdef TestConnection < tests.Prep % TestConnection tests typical connection scenarios. methods (Test) - function testConnection(testCase) + function TestConnection_testConnection(testCase) st = dbstack; disp(['---------------' st(1).name '---------------']); testCase.verifyTrue(dj.conn(... @@ -9,26 +9,35 @@ function testConnection(testCase) testCase.CONN_INFO.user,... testCase.CONN_INFO.password,'',true).isConnected); end - function testConnectionExists(testCase) + function TestConnection_testConnectionExists(testCase) % testConnectionExists tests that will not fail if connection open % to the same host. % Fix https://github.com/datajoint/datajoint-matlab/issues/160 st = dbstack; disp(['---------------' st(1).name '---------------']); - dj.conn(testCase.CONN_INFO.host, '', '', '', '', true) - dj.conn(testCase.CONN_INFO.host, '', '', '', '', true) + dj.conn(testCase.CONN_INFO.host, '', '', '', '', true); + dj.conn(testCase.CONN_INFO.host, '', '', '', '', true); end - function testConnectionDiffHost(testCase) + function TestConnection_testConnectionDiffHost(testCase) % testConnectionDiffHost tests that will fail if connection open % to a different host. % Fix https://github.com/datajoint/datajoint-matlab/issues/160 st = dbstack; disp(['---------------' st(1).name '---------------']); - dj.conn(testCase.CONN_INFO.host, '', '', '', '', true) + dj.conn(testCase.CONN_INFO.host, '', '', '', '', true); testCase.verifyError(@() dj.conn(... 'anything', '', '', '', '', true), ... 'DataJoint:Connection:AlreadyInstantiated'); end + function TestConnection_testPort(testCase) + st = dbstack; + disp(['---------------' st(1).name '---------------']); + testCase.verifyError(@() dj.conn(... + [testCase.CONN_INFO.host ':3307'], ... + testCase.CONN_INFO.user,... + testCase.CONN_INFO.password,'',true), ... + 'MySQL:Error'); + end end end \ No newline at end of file diff --git a/+tests/TestRelationalOperand.m b/+tests/TestRelationalOperand.m new file mode 100644 index 00000000..caf31207 --- /dev/null +++ b/+tests/TestRelationalOperand.m @@ -0,0 +1,36 @@ +classdef TestRelationalOperand < tests.Prep + % TestRelationalOperand tests relational operations. + methods (Test) + function TestRelationalOperand_testUpdateDate(testCase) + st = dbstack; + disp(['---------------' st(1).name '---------------']); + % https://github.com/datajoint/datajoint-matlab/issues/211 + package = 'University'; + + c1 = dj.conn(... + testCase.CONN_INFO.host,... + testCase.CONN_INFO.user,... + testCase.CONN_INFO.password,'',true); + + dj.createSchema(package,[testCase.test_root '/test_schemas'], ... + [testCase.PREFIX '_university']); + + insert(University.All, struct( ... + 'id', 2, ... + 'date', '2019-12-20' ... + )); + q = University.All & 'id=2'; + + new_value = ''; + q.update('date', new_value); + testCase.verifyEqual(q.fetch1('date'), new_value); + + new_value = '2020-04-14'; + q.update('date', new_value); + testCase.verifyEqual(q.fetch1('date'), new_value); + + q.update('date'); + testCase.verifyEqual(q.fetch1('date'), ''); + end + end +end \ No newline at end of file diff --git a/+tests/TestTls.m b/+tests/TestTls.m index 9da70f8a..a340afb6 100644 --- a/+tests/TestTls.m +++ b/+tests/TestTls.m @@ -1,7 +1,7 @@ -classdef TestTls < matlab.unittest.TestCase +classdef TestTls < tests.Prep % TestTls tests TLS connection scenarios. methods (Test) - function testSecureConn(testCase) + function TestTls_testSecureConn(testCase) % secure connection test st = dbstack; disp(['---------------' st(1).name '---------------']); @@ -12,7 +12,7 @@ function testSecureConn(testCase) '',true,true).query(... 'SHOW STATUS LIKE ''Ssl_cipher''').Value{1}) > 0); end - function testInsecureConn(testCase) + function TestTls_testInsecureConn(testCase) % insecure connection test st = dbstack; disp(['---------------' st(1).name '---------------']); @@ -24,7 +24,7 @@ function testInsecureConn(testCase) 'SHOW STATUS LIKE ''Ssl_cipher''').Value{1}, ... ''); end - function testPreferredConn(testCase) + function TestTls_testPreferredConn(testCase) % preferred connection test st = dbstack; disp(['---------------' st(1).name '---------------']); @@ -35,7 +35,7 @@ function testPreferredConn(testCase) '',true).query(... 'SHOW STATUS LIKE ''Ssl_cipher''').Value{1}) > 0); end - function testRejectException(testCase) + function TestTls_testRejectException(testCase) % test exception on require TLS st = dbstack; disp(['---------------' st(1).name '---------------']); @@ -54,7 +54,7 @@ function testRejectException(testCase) ["requires secure connection","Access denied"])); %MySQL8,MySQL5 end end - function testStructException(testCase) + function TestTls_testStructException(testCase) % test exception on TLS struct st = dbstack; disp(['---------------' st(1).name '---------------']); @@ -63,7 +63,7 @@ function testStructException(testCase) testCase.CONN_INFO.user, ... testCase.CONN_INFO.password, ... '',true,struct('ca','fake/path/some/where')), ... - 'DataJoint:TLS:InvalidStruct'); + 'mYm:TLS:InvalidStruct'); end end end \ No newline at end of file diff --git a/+tests/test_schemas/+University/All.m b/+tests/test_schemas/+University/All.m new file mode 100644 index 00000000..55d4be71 --- /dev/null +++ b/+tests/test_schemas/+University/All.m @@ -0,0 +1,12 @@ +%{ +# All +id : int +--- +string=null : varchar(30) +datetime=null : datetime +date=null : date +number=null : float +blob=null : longblob +%} +classdef All < dj.Manual +end \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3a9626a7..c9a8c43f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ mym/ *.mltbx *.env +notebook +*getSchema.m +docker-compose.yml +.vscode +matlab.prf +win.* +macos.* \ No newline at end of file