Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options for foreign key attributes #302

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions +dj/+internal/Declare.m
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,14 @@ case strncmp(line,'---',3)
inKey = false;
% foreign key
case regexp(line, '^(\s*\([^)]+\)\s*)?->.+$')
[fk_attr_sql, fk_sql, newFields] = dj.internal.Declare.makeFK( ...
line, fields, inKey, ...
dj.internal.shorthash(sprintf('`%s`.`%s`', ...
table_instance.schema.dbname, tableName)));
[fk_attr_sql, fk_sql, newFields, idx_sql] = ...
dj.internal.Declare.makeFK( ...
line, fields, inKey, ...
dj.internal.shorthash(sprintf('`%s`.`%s`', ...
table_instance.schema.dbname, tableName)));
attributeSql = [attributeSql, fk_attr_sql]; %#ok<AGROW>
foreignKeySql = [foreignKeySql, fk_sql]; %#ok<AGROW>
indexSql = [indexSql, idx_sql]; %#ok<AGROW>
fields = [fields, newFields]; %#ok<AGROW>
if inKey
primaryFields = [primaryFields, newFields]; %#ok<AGROW>
Expand Down Expand Up @@ -236,7 +238,8 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
fieldInfo.isnullable = strcmpi(fieldInfo.default,'null');
end

function [all_attr_sql, fk_sql, newattrs] = makeFK(line, existingFields, inKey, hash)
function [all_attr_sql, fk_sql, newattrs, idx_sql] = makeFK(line, existingFields, ...
inKey, hash)
% [sql, newattrs] = MAKEFK(sql, line, existingFields, inKey, hash)
% Add foreign key to SQL table definition.
% sql: <string> Modified in-place SQL to include foreign keys.
Expand All @@ -247,8 +250,11 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
% hash: <string> Current hash as base.
fk_sql = '';
all_attr_sql = '';
idx_sql = '';
pat = ['^(?<newattrs>\([\s\w,]*\))?' ...
'\s*->\s*' ...
'(?<options>\[[\s\w,]*\])?' ...
'\s*' ...
'(?<cname>\w+\.[A-Z][A-Za-z0-9]*)' ...
'\w*' ...
'(?<attrs>\([\s\w,]*\))?' ...
Expand All @@ -263,8 +269,10 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name

% parse and validate the attribute lists
attrs = strsplit(fk.attrs, {' ',',','(',')'});
options = strsplit(fk.options, {' ',',','[',']'});
newattrs = strsplit(fk.newattrs, {' ',',','(',')'});
attrs(cellfun(@isempty, attrs))=[];
options(cellfun(@isempty, options))=[];
newattrs(cellfun(@isempty, newattrs))=[];
assert(all(cellfun(@(a) ismember(a, rel.primaryKey), attrs)), ...
'All attributes in (%s) must be in the primary key of %s', ...
Expand Down Expand Up @@ -298,7 +306,10 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
fieldInfo = rel.tableHeader.attributes(strcmp(attrs{i}, ...
rel.tableHeader.names));
fieldInfo.name = newattrs{i};
fieldInfo.nullabe = ~inKey; % nonprimary references are nullable
assert(~inKey || ~any(strcmpi('NULLABLE', options)), ...
sprintf(['Primary dependencies cannot be ' ...
'nullable in line "%s"'], line));
fieldInfo.isnullable = logical(~inKey*any(strcmpi('NULLABLE', options)));
[attr_sql, ~, ~] = dj.internal.Declare.compileAttribute(fieldInfo, []);
all_attr_sql = sprintf('%s%s,\n', all_attr_sql, attr_sql);
end
Expand All @@ -311,6 +322,9 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
['%sCONSTRAINT `%s` FOREIGN KEY (%s) REFERENCES %s (%s) ' ...
'ON UPDATE CASCADE ON DELETE RESTRICT'], fk_sql, hash, ...
backquotedList(fkattrs), rel.fullTableName, backquotedList(rel.primaryKey));
if any(strcmpi('UNIQUE', options))
idx_sql = sprintf('UNIQUE INDEX (%s)', ['`' strjoin(newattrs, '`,`') '`']);
end
end

function [field, foreignKeySql] = substituteSpecialType(field, category, foreignKeySql)
Expand Down
10 changes: 4 additions & 6 deletions +dj/+internal/Table.m
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,8 @@ function erd(self, up, down)
self.schema.conn.tableToClass(fk(i).ref));
else
ref_attr = setdiff(fk(i).attrs, fk(i).ref_attrs);
assert(length(ref_attr)==1, ...
'only single-attributes aliases are supported for now')
str = sprintf('%s\n (%s) -> %s', str, ref_attr{1}, ...
self.schema.conn.tableToClass(fk(i).ref));
str = sprintf('%s\n (%s) -> %s', str, strjoin(ref_attr, ', '), ...
self.schema.conn.tableToClass(fk(i).ref));
end
end
fk(resolved) = [];
Expand Down Expand Up @@ -369,8 +367,8 @@ function addForeignKey(self, target)
if isa(target, 'dj.Table')
target = sprintf('->%s', target.className);
end
[attr_sql, fk_sql, ~] = dj.internal.Declare.makeFK('', target, self.primaryKey, ...
true, dj.internal.shorthash(self.fullTableName));
[attr_sql, fk_sql, ~, ~] = dj.internal.Declare.makeFK('', target, ...
self.primaryKey, true, dj.internal.shorthash(self.fullTableName));
self.alter(sprintf('ADD %s%s', attr_sql, fk_sql))
end

Expand Down
2 changes: 1 addition & 1 deletion LNX-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ services:
interval: 1s
fakeservices.datajoint.io:
<<: *net
image: raphaelguzman/nginx:v0.0.8
image: raphaelguzman/nginx:v0.0.10
environment:
- ADD_db_TYPE=DATABASE
- ADD_db_ENDPOINT=db:3306
Expand Down
2 changes: 1 addition & 1 deletion local-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ services:
interval: 1s
fakeservices.datajoint.io:
<<: *net
image: raphaelguzman/nginx:v0.0.8
image: raphaelguzman/nginx:v0.0.10
environment:
- ADD_db_TYPE=DATABASE
- ADD_db_ENDPOINT=db:3306
Expand Down
1 change: 1 addition & 0 deletions tests/Main.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TestExternalS3 & ...
TestFetch & ...
TestProjection & ...
TestRelationalOperand & ...
TestSchema & ...
TestTls & ...
TestUuid
Expand Down
64 changes: 64 additions & 0 deletions tests/TestRelationalOperand.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
classdef TestRelationalOperand < Prep
% TestRelationalOperand tests relational operations.
methods (Test)
function TestRelationalOperand_testFkOptions(testCase)
st = dbstack;
disp(['---------------' st(1).name '---------------']);
% https://github.com/datajoint/datajoint-matlab/issues/110
package = 'Lab';

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 '_lab']);

insert(Lab.Subject, {
0, '2020-04-02';
1, '2020-05-03';
2, '2020-04-22';
});
insert(Lab.Rig, struct( ...
'rig_manufacturer', 'Lenovo', ...
'rig_model', 'ThinkPad', ...
'rig_note', 'blah' ...
));
% insert as renamed foreign keys
insert(Lab.ActiveSession, struct( ...
'subject_id', 0, ...
'session_rig_class', 'Lenovo', ...
'session_rig_id', 'ThinkPad' ...
));
testCase.verifyEqual(fetch1(Lab.ActiveSession, 'session_rig_class'), 'Lenovo');
% insert null for rig (subject reserved, awaiting rig assignment)
insert(Lab.ActiveSession, struct( ...
'subject_id', 1 ...
));
testCase.verifyTrue(isempty(fetch1(Lab.ActiveSession & 'subject_id=1', ...
'session_rig_class')));
% insert duplicate rig (rigs should only be active once per
% subject)
try
insert(Lab.ActiveSession, struct( ...
'subject_id', 2, ...
'session_rig_class', 'Lenovo', ...
'session_rig_id', 'ThinkPad' ...
));
error('Unique index fail...');
catch ME
if ~contains(ME.message, 'Duplicate entry')
rethrow(ME);
end
end
% verify reverse engineering (TBD)
q = Lab.ActiveSession;
raw_def = dj.internal.Declare.getDefinition(q);
assembled_def = describe(q);
[raw_sql, ~] = dj.internal.Declare.declare(q, raw_def);
% [assembled_sql, ~] = dj.internal.Declare.declare(q, assembled_def);
% testCase.verifyEqual(raw_sql, assembled_sql);
end
end
end
8 changes: 8 additions & 0 deletions tests/test_schemas/+Lab/ActiveSession.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
%{
# ActiveSession
-> [unique] Lab.Subject
---
(session_rig_class, session_rig_id) -> [nullable, unique] Lab.Rig(rig_manufacturer, rig_model)
%}
classdef ActiveSession < dj.Manual
end
9 changes: 9 additions & 0 deletions tests/test_schemas/+Lab/Rig.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
%{
# Rig
rig_manufacturer: varchar(50)
rig_model: varchar(30)
---
rig_note : varchar(100)
%}
classdef Rig < dj.Manual
end
9 changes: 9 additions & 0 deletions tests/test_schemas/+Lab/Subject.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
%{
# Subject
subject_id : int
---
subject_dob : date
unique index(subject_dob)
%}
classdef Subject < dj.Manual
end