Skip to content

Commit 508f0d1

Browse files
authored
Merge pull request #303 from datajoint/stage-external-storage
Add options for foreign key attributes
2 parents a43409d + 5a9df6d commit 508f0d1

File tree

7 files changed

+116
-12
lines changed

7 files changed

+116
-12
lines changed

+dj/+internal/Declare.m

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,14 @@ case strncmp(line,'---',3)
135135
inKey = false;
136136
% foreign key
137137
case regexp(line, '^(\s*\([^)]+\)\s*)?->.+$')
138-
[fk_attr_sql, fk_sql, newFields] = dj.internal.Declare.makeFK( ...
139-
line, fields, inKey, ...
140-
dj.internal.shorthash(sprintf('`%s`.`%s`', ...
141-
table_instance.schema.dbname, tableName)));
138+
[fk_attr_sql, fk_sql, newFields, idx_sql] = ...
139+
dj.internal.Declare.makeFK( ...
140+
line, fields, inKey, ...
141+
dj.internal.shorthash(sprintf('`%s`.`%s`', ...
142+
table_instance.schema.dbname, tableName)));
142143
attributeSql = [attributeSql, fk_attr_sql]; %#ok<AGROW>
143144
foreignKeySql = [foreignKeySql, fk_sql]; %#ok<AGROW>
145+
indexSql = [indexSql, idx_sql]; %#ok<AGROW>
144146
fields = [fields, newFields]; %#ok<AGROW>
145147
if inKey
146148
primaryFields = [primaryFields, newFields]; %#ok<AGROW>
@@ -236,7 +238,8 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
236238
fieldInfo.isnullable = strcmpi(fieldInfo.default,'null');
237239
end
238240

239-
function [all_attr_sql, fk_sql, newattrs] = makeFK(line, existingFields, inKey, hash)
241+
function [all_attr_sql, fk_sql, newattrs, idx_sql] = makeFK(line, existingFields, ...
242+
inKey, hash)
240243
% [sql, newattrs] = MAKEFK(sql, line, existingFields, inKey, hash)
241244
% Add foreign key to SQL table definition.
242245
% sql: <string> Modified in-place SQL to include foreign keys.
@@ -247,8 +250,11 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
247250
% hash: <string> Current hash as base.
248251
fk_sql = '';
249252
all_attr_sql = '';
253+
idx_sql = '';
250254
pat = ['^(?<newattrs>\([\s\w,]*\))?' ...
251255
'\s*->\s*' ...
256+
'(?<options>\[[\s\w,]*\])?' ...
257+
'\s*' ...
252258
'(?<cname>\w+\.[A-Z][A-Za-z0-9]*)' ...
253259
'\w*' ...
254260
'(?<attrs>\([\s\w,]*\))?' ...
@@ -263,12 +269,17 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
263269

264270
% parse and validate the attribute lists
265271
attrs = strsplit(fk.attrs, {' ',',','(',')'});
272+
options = strsplit(fk.options, {' ',',','[',']'});
266273
newattrs = strsplit(fk.newattrs, {' ',',','(',')'});
267274
attrs(cellfun(@isempty, attrs))=[];
275+
options(cellfun(@isempty, options))=[];
268276
newattrs(cellfun(@isempty, newattrs))=[];
269277
assert(all(cellfun(@(a) ismember(a, rel.primaryKey), attrs)), ...
270278
'All attributes in (%s) must be in the primary key of %s', ...
271279
strjoin(attrs, ','), rel.className)
280+
assert(~inKey || ~any(strcmpi('NULLABLE', options)), ...
281+
sprintf(['Primary dependencies cannot be ' ...
282+
'nullable in line "%s"'], line));
272283
if length(newattrs)==1
273284
% unambiguous single attribute
274285
if length(rel.primaryKey)==1
@@ -298,7 +309,7 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
298309
fieldInfo = rel.tableHeader.attributes(strcmp(attrs{i}, ...
299310
rel.tableHeader.names));
300311
fieldInfo.name = newattrs{i};
301-
fieldInfo.nullabe = ~inKey; % nonprimary references are nullable
312+
fieldInfo.isnullable = logical(~inKey*any(strcmpi('NULLABLE', options)));
302313
[attr_sql, ~, ~] = dj.internal.Declare.compileAttribute(fieldInfo, []);
303314
all_attr_sql = sprintf('%s%s,\n', all_attr_sql, attr_sql);
304315
end
@@ -311,6 +322,9 @@ case regexp(line, ['^[a-z][a-z\d_]*\s*' ... % name
311322
['%sCONSTRAINT `%s` FOREIGN KEY (%s) REFERENCES %s (%s) ' ...
312323
'ON UPDATE CASCADE ON DELETE RESTRICT'], fk_sql, hash, ...
313324
backquotedList(fkattrs), rel.fullTableName, backquotedList(rel.primaryKey));
325+
if any(strcmpi('UNIQUE', options))
326+
idx_sql = sprintf('UNIQUE INDEX (%s)', ['`' strjoin(newattrs, '`,`') '`']);
327+
end
314328
end
315329

316330
function [field, foreignKeySql] = substituteSpecialType(field, category, foreignKeySql)

+dj/+internal/Table.m

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,8 @@ function erd(self, up, down)
269269
self.schema.conn.tableToClass(fk(i).ref));
270270
else
271271
ref_attr = setdiff(fk(i).attrs, fk(i).ref_attrs);
272-
assert(length(ref_attr)==1, ...
273-
'only single-attributes aliases are supported for now')
274-
str = sprintf('%s\n (%s) -> %s', str, ref_attr{1}, ...
275-
self.schema.conn.tableToClass(fk(i).ref));
272+
str = sprintf('%s\n (%s) -> %s', str, strjoin(ref_attr, ', '), ...
273+
self.schema.conn.tableToClass(fk(i).ref));
276274
end
277275
end
278276
fk(resolved) = [];
@@ -369,8 +367,8 @@ function addForeignKey(self, target)
369367
if isa(target, 'dj.Table')
370368
target = sprintf('->%s', target.className);
371369
end
372-
[attr_sql, fk_sql, ~] = dj.internal.Declare.makeFK('', target, self.primaryKey, ...
373-
true, dj.internal.shorthash(self.fullTableName));
370+
[attr_sql, fk_sql, ~, ~] = dj.internal.Declare.makeFK('', target, ...
371+
self.primaryKey, true, dj.internal.shorthash(self.fullTableName));
374372
self.alter(sprintf('ADD %s%s', attr_sql, fk_sql))
375373
end
376374

tests/Main.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
TestExternalS3 & ...
77
TestFetch & ...
88
TestProjection & ...
9+
TestRelationalOperand & ...
910
TestSchema & ...
1011
TestTls & ...
1112
TestUuid

tests/TestRelationalOperand.m

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
classdef TestRelationalOperand < Prep
2+
% TestRelationalOperand tests relational operations.
3+
methods (Test)
4+
function TestRelationalOperand_testFkOptions(testCase)
5+
st = dbstack;
6+
disp(['---------------' st(1).name '---------------']);
7+
% https://github.com/datajoint/datajoint-matlab/issues/110
8+
package = 'Lab';
9+
10+
c1 = dj.conn(...
11+
testCase.CONN_INFO.host,...
12+
testCase.CONN_INFO.user,...
13+
testCase.CONN_INFO.password,'',true);
14+
15+
dj.createSchema(package,[testCase.test_root '/test_schemas'], ...
16+
[testCase.PREFIX '_lab']);
17+
18+
insert(Lab.Subject, {
19+
0, '2020-04-02';
20+
1, '2020-05-03';
21+
2, '2020-04-22';
22+
});
23+
insert(Lab.Rig, struct( ...
24+
'rig_manufacturer', 'Lenovo', ...
25+
'rig_model', 'ThinkPad', ...
26+
'rig_note', 'blah' ...
27+
));
28+
% insert as renamed foreign keys
29+
insert(Lab.ActiveSession, struct( ...
30+
'subject_id', 0, ...
31+
'session_rig_class', 'Lenovo', ...
32+
'session_rig_id', 'ThinkPad' ...
33+
));
34+
testCase.verifyEqual(fetch1(Lab.ActiveSession, 'session_rig_class'), 'Lenovo');
35+
% insert null for rig (subject reserved, awaiting rig assignment)
36+
insert(Lab.ActiveSession, struct( ...
37+
'subject_id', 1 ...
38+
));
39+
testCase.verifyTrue(isempty(fetch1(Lab.ActiveSession & 'subject_id=1', ...
40+
'session_rig_class')));
41+
% insert duplicate rig (rigs should only be active once per
42+
% subject)
43+
try
44+
insert(Lab.ActiveSession, struct( ...
45+
'subject_id', 2, ...
46+
'session_rig_class', 'Lenovo', ...
47+
'session_rig_id', 'ThinkPad' ...
48+
));
49+
error('Unique index fail...');
50+
catch ME
51+
if ~contains(ME.message, 'Duplicate entry')
52+
rethrow(ME);
53+
end
54+
end
55+
% verify reverse engineering
56+
% (pending https://github.com/datajoint/datajoint-matlab/issues/305 solution)
57+
q = Lab.ActiveSession;
58+
raw_def = dj.internal.Declare.getDefinition(q);
59+
assembled_def = describe(q);
60+
[raw_sql, ~] = dj.internal.Declare.declare(q, raw_def);
61+
% [assembled_sql, ~] = dj.internal.Declare.declare(q, assembled_def);
62+
% testCase.verifyEqual(raw_sql, assembled_sql);
63+
end
64+
end
65+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
%{
2+
# ActiveSession
3+
-> [unique] Lab.Subject
4+
---
5+
(session_rig_class, session_rig_id) -> [nullable, unique] Lab.Rig(rig_manufacturer, rig_model)
6+
%}
7+
classdef ActiveSession < dj.Manual
8+
end

tests/test_schemas/+Lab/Rig.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
%{
2+
# Rig
3+
rig_manufacturer: varchar(50)
4+
rig_model: varchar(30)
5+
---
6+
rig_note : varchar(100)
7+
%}
8+
classdef Rig < dj.Manual
9+
end

tests/test_schemas/+Lab/Subject.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
%{
2+
# Subject
3+
subject_id : int
4+
---
5+
subject_dob : date
6+
unique index(subject_dob)
7+
%}
8+
classdef Subject < dj.Manual
9+
end

0 commit comments

Comments
 (0)