• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

datajoint / datajoint-python / 1476

pending completion
1476

Pull #568

travis-ci

web-flow
fix foreign keys with UUID-based foreign keys
Pull Request #568: fix foreign keys with UUID-based foreign keys

16 of 16 new or added lines in 5 files covered. (100.0%)

2231 of 2469 relevant lines covered (90.36%)

3.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.69
/datajoint/table.py
1
import collections
4✔
2
import itertools
4✔
3
import inspect
4✔
4
import platform
4✔
5
import numpy as np
4✔
6
import pandas
4✔
7
import pymysql
4✔
8
import logging
4✔
9
import uuid
4✔
10
from pymysql import OperationalError, InternalError, IntegrityError
4✔
11
from .settings import config
4✔
12
from .declare import declare
4✔
13
from .expression import QueryExpression
4✔
14
from . import attach, blob
4✔
15
from .utils import user_choice
4✔
16
from .heading import Heading
4✔
17
from .errors import server_error_codes, DataJointError, DuplicateError
4✔
18
from .version import __version__ as version
4✔
19

20
logger = logging.getLogger(__name__)
4✔
21

22

23
class _rename_map(tuple):
4✔
24
    """ for internal use """
25
    pass
4✔
26

27

28
class Table(QueryExpression):
4✔
29
    """
30
    Table is an abstract class that represents a base relation, i.e. a table in the schema.
31
    To make it a concrete class, override the abstract properties specifying the connection,
32
    table name, database, and definition.
33
    A Relation implements insert and delete methods in addition to inherited relational operators.
34
    """
35
    _heading = None
4✔
36
    database = None
4✔
37
    _log_ = None
4✔
38
    _external_table = None
4✔
39

40
    # -------------- required by QueryExpression ----------------- #
41
    @property
4✔
42
    def heading(self):
43
        """
44
        Returns the table heading. If the table is not declared, attempts to declare it and return heading.
45
        :return: table heading
46
        """
47
        if self._heading is None:
4✔
48
            self._heading = Heading()  # instance-level heading
4✔
49
        if not self._heading:  # lazy loading of heading
4✔
50
            if self.connection is None:
4✔
51
                raise DataJointError(
×
52
                    'DataJoint class is missing a database connection. '
53
                    'Missing schema decorator on the class? (e.g. @schema)')
54
            else:
55
                self._heading.init_from_database(self.connection, self.database, self.table_name)
4✔
56
        return self._heading
4✔
57

58
    def declare(self, context=None):
4✔
59
        """
60
        Use self.definition to declare the table in the schema.
61
        """
62
        if self.connection.in_transaction:
4✔
63
            raise DataJointError('Cannot declare new tables inside a transaction, '
×
64
                                 'e.g. from inside a populate/make call')
65
        try:
4✔
66
            sql, uses_external = declare(self.full_table_name, self.definition, context)
4✔
67
            if uses_external:
4✔
68
                sql = sql.format(external_table=self.external_table.full_table_name)
4✔
69
            self.connection.query(sql)
4✔
70
        except pymysql.OperationalError as error:
4✔
71
            # skip if no create privilege
72
            if error.args[0] == server_error_codes['command denied']:
4✔
73
                logger.warning(error.args[1])
4✔
74
            else:
75
                raise
×
76
        else:
77
            self._log('Declared ' + self.full_table_name)
4✔
78

79
    @property
4✔
80
    def from_clause(self):
81
        """
82
        :return: the FROM clause of SQL SELECT statements.
83
        """
84
        return self.full_table_name
4✔
85

86
    def get_select_fields(self, select_fields=None):
4✔
87
        """
88
        :return: the selected attributes from the SQL SELECT statement.
89
        """
90
        return '*' if select_fields is None else self.heading.project(select_fields).as_sql
4✔
91

92
    def parents(self, primary=None):
4✔
93
        """
94
        :param primary: if None, then all parents are returned. If True, then only foreign keys composed of
95
            primary key attributes are considered.  If False, the only foreign keys including at least one non-primary
96
            attribute are considered.
97
        :return: dict of tables referenced with self's foreign keys
98
        """
99
        return self.connection.dependencies.parents(self.full_table_name, primary)
4✔
100

101
    def children(self, primary=None):
4✔
102
        """
103
        :param primary: if None, then all parents are returned. If True, then only foreign keys composed of
104
            primary key attributes are considered.  If False, the only foreign keys including at least one non-primary
105
            attribute are considered.
106
        :return: dict of tables with foreign keys referencing self
107
        """
108
        return self.connection.dependencies.children(self.full_table_name, primary)
4✔
109

110
    def descendants(self):
4✔
111
        return self. connection.dependencies.descendants(self.full_table_name)
4✔
112

113
    def ancestors(self):
4✔
114
        return self. connection.dependencies.ancestors(self.full_table_name)
4✔
115

116
    @property
4✔
117
    def is_declared(self):
118
        """
119
        :return: True is the table is declared in the schema.
120
        """
121
        return self.connection.query(
4✔
122
            'SHOW TABLES in `{database}` LIKE "{table_name}"'.format(
123
                database=self.database, table_name=self.table_name)).rowcount > 0
124

125
    @property
4✔
126
    def full_table_name(self):
127
        """
128
        :return: full table name in the schema
129
        """
130
        return r"`{0:s}`.`{1:s}`".format(self.database, self.table_name)
4✔
131

132
    @property
4✔
133
    def _log(self):
134
        if self._log_ is None:
4✔
135
            self._log_ = Log(self.connection, database=self.database)
4✔
136
        return self._log_
4✔
137

138
    @property
4✔
139
    def external_table(self):
140
        if self._external_table is None:
4✔
141
            # trigger the creation of the external hash lookup for the current schema
142
            self._external_table = self.connection.schemas[self.database].external_table
4✔
143
        return self._external_table
4✔
144

145
    def insert1(self, row, **kwargs):
4✔
146
        """
147
        Insert one data record or one Mapping (like a dict).
148
        :param row: a numpy record, a dict-like object, or an ordered sequence to be inserted as one row.
149
        For kwargs, see insert()
150
        """
151
        self.insert((row,), **kwargs)
4✔
152

153
    def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False, allow_direct_insert=None):
4✔
154
        """
155
        Insert a collection of rows.
156

157
        :param rows: An iterable where an element is a numpy record, a dict-like object, a pandas.DataFrame, a sequence,
158
            or a query expression with the same heading as table self.
159
        :param replace: If True, replaces the existing tuple.
160
        :param skip_duplicates: If True, silently skip duplicate inserts.
161
        :param ignore_extra_fields: If False, fields that are not in the heading raise error.
162
        :param allow_direct_insert: applies only in auto-populated tables. Set True to insert outside populate calls.
163

164
        Example::
165
        >>> relation.insert([
166
        >>>     dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
167
        >>>     dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")])
168
        """
169

170
        if isinstance(rows, pandas.DataFrame):
4✔
171
            rows = rows.to_records()
4✔
172

173
        # prohibit direct inserts into auto-populated tables
174
        if not (allow_direct_insert or getattr(self, '_allow_insert', True)):  # _allow_insert is only present in AutoPopulate
4✔
175
            raise DataJointError(
4✔
176
                'Auto-populate tables can only be inserted into from their make methods during populate calls.'
177
                ' To override, use the the allow_direct_insert argument.')
178

179
        heading = self.heading
4✔
180
        if inspect.isclass(rows) and issubclass(rows, QueryExpression):   # instantiate if a class
4✔
181
            rows = rows()
4✔
182
        if isinstance(rows, QueryExpression):
4✔
183
            # insert from select
184
            if not ignore_extra_fields:
4✔
185
                try:
4✔
186
                    raise DataJointError(
4✔
187
                        "Attribute %s not found. To ignore extra attributes in insert, set ignore_extra_fields=True." %
188
                        next(name for name in rows.heading if name not in heading))
189
                except StopIteration:
4✔
190
                    pass
4✔
191
            fields = list(name for name in rows.heading if name in heading)
4✔
192
            query = '{command} INTO {table} ({fields}) {select}{duplicate}'.format(
4✔
193
                command='REPLACE' if replace else 'INSERT',
194
                fields='`' + '`,`'.join(fields) + '`',
195
                table=self.full_table_name,
196
                select=rows.make_sql(select_fields=fields),
197
                duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`'.format(
198
                    table=self.full_table_name, pk=self.primary_key[0])
199
                           if skip_duplicates else ''))
200
            self.connection.query(query)
4✔
201
            return
4✔
202

203
        if heading.attributes is None:
4✔
204
            logger.warning('Could not access table {table}'.format(table=self.full_table_name))
×
205
            return
×
206

207
        field_list = None  # ensures that all rows have the same attributes in the same order as the first row.
4✔
208

209
        def make_row_to_insert(row):
4✔
210
            """
211
            :param row:  A tuple to insert
212
            :return: a dict with fields 'names', 'placeholders', 'values'
213
            """
214

215
            def make_placeholder(name, value):
4✔
216
                """
217
                For a given attribute `name` with `value`, return its processed value or value placeholder
218
                as a string to be included in the query and the value, if any, to be submitted for
219
                processing by mysql API.
220
                :param name:  name of attribute to be inserted
221
                :param value: value of attribute to be inserted
222
                """
223
                if ignore_extra_fields and name not in heading:
4✔
224
                    return None
×
225
                attr = heading[name]
4✔
226
                if value is None or (attr.numeric and (value == '' or np.isnan(np.float(value)))):
4✔
227
                    # set default value
228
                    placeholder, value = 'DEFAULT', None
4✔
229
                else:  # not NULL
230
                    placeholder = '%s'
4✔
231
                    if attr.uuid:
4✔
232
                        if not isinstance(value, uuid.UUID):
4✔
233
                            raise DataJointError('The value of attribute `%s` must be of type UUID' % attr.name)
×
234
                        value = value.bytes
4✔
235
                    elif attr.is_blob:
4✔
236
                        value = blob.pack(value)
4✔
237
                        value = self.external_table.put(attr.type, value) if attr.is_external else value
4✔
238
                    elif attr.is_attachment:
4✔
239
                        value = attach.load(value)
4✔
240
                        value = self.external_table.put(attr.type, value) if attr.is_external else value
4✔
241
                    elif attr.numeric:
4✔
242
                        value = str(int(value) if isinstance(value, bool) else value)
4✔
243
                return name, placeholder, value
4✔
244

245
            def check_fields(fields):
4✔
246
                """
247
                Validates that all items in `fields` are valid attributes in the heading
248
                :param fields: field names of a tuple
249
                """
250
                if field_list is None:
4✔
251
                    if not ignore_extra_fields:
4✔
252
                        for field in fields:
4✔
253
                            if field not in heading:
4✔
254
                                raise KeyError(u'`{0:s}` is not in the table heading'.format(field))
4✔
255
                elif set(field_list) != set(fields).intersection(heading.names):
4✔
256
                    raise DataJointError('Attempt to insert rows with different fields')
4✔
257

258
            if isinstance(row, np.void):  # np.array
4✔
259
                check_fields(row.dtype.fields)
4✔
260
                attributes = [make_placeholder(name, row[name])
4✔
261
                              for name in heading if name in row.dtype.fields]
262
            elif isinstance(row, collections.abc.Mapping):  # dict-based
4✔
263
                check_fields(row)
4✔
264
                attributes = [make_placeholder(name, row[name]) for name in heading if name in row]
4✔
265
            else:  # positional
266
                try:
4✔
267
                    if len(row) != len(heading):
4✔
268
                        raise DataJointError(
4✔
269
                            'Invalid insert argument. Incorrect number of attributes: '
270
                            '{given} given; {expected} expected'.format(
271
                                given=len(row), expected=len(heading)))
272
                except TypeError:
4✔
273
                    raise DataJointError('Datatype %s cannot be inserted' % type(row))
4✔
274
                else:
275
                    attributes = [make_placeholder(name, value) for name, value in zip(heading, row)]
4✔
276
            if ignore_extra_fields:
4✔
277
                attributes = [a for a in attributes if a is not None]
4✔
278

279
            assert len(attributes), 'Empty tuple'
4✔
280
            row_to_insert = dict(zip(('names', 'placeholders', 'values'), zip(*attributes)))
4✔
281
            nonlocal field_list
282
            if field_list is None:
4✔
283
                # first row sets the composition of the field list
284
                field_list = row_to_insert['names']
4✔
285
            else:
286
                #  reorder attributes in row_to_insert to match field_list
287
                order = list(row_to_insert['names'].index(field) for field in field_list)
4✔
288
                row_to_insert['names'] = list(row_to_insert['names'][i] for i in order)
4✔
289
                row_to_insert['placeholders'] = list(row_to_insert['placeholders'][i] for i in order)
4✔
290
                row_to_insert['values'] = list(row_to_insert['values'][i] for i in order)
4✔
291

292
            return row_to_insert
4✔
293

294
        rows = list(make_row_to_insert(row) for row in rows)
4✔
295
        if rows:
4✔
296
            try:
4✔
297
                query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format(
4✔
298
                    command='REPLACE' if replace else 'INSERT',
299
                    destination=self.from_clause,
300
                    fields='`,`'.join(field_list),
301
                    placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows),
302
                    duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0])
303
                               if skip_duplicates else ''))
304
                self.connection.query(query, args=list(
4✔
305
                    itertools.chain.from_iterable((v for v in r['values'] if v is not None) for r in rows)))
306
            except (OperationalError, InternalError, IntegrityError) as err:
4✔
307
                if err.args[0] == server_error_codes['command denied']:
4✔
308
                    raise DataJointError('Command denied:  %s' % err.args[1]) from None
4✔
309
                elif err.args[0] == server_error_codes['unknown column']:
4✔
310
                    # args[1] -> Unknown column 'extra' in 'field list'
311
                    raise DataJointError(
×
312
                        '{} : To ignore extra fields, set ignore_extra_fields=True in insert.'.format(err.args[1])
313
                    ) from None
314
                elif err.args[0] == server_error_codes['duplicate entry']:
4✔
315
                    raise DuplicateError(
4✔
316
                        '{} : To ignore duplicate entries, set skip_duplicates=True in insert.'.format(err.args[1])
317
                    ) from None
318
                else:
319
                    raise
4✔
320

321
    def delete_quick(self, get_count=False):
4✔
322
        """
323
        Deletes the table without cascading and without user prompt.
324
        If this table has populated dependent tables, this will fail.
325
        """
326
        query = 'DELETE FROM ' + self.full_table_name + self.where_clause
4✔
327
        self.connection.query(query)
4✔
328
        count = self.connection.query("SELECT ROW_COUNT()").fetchone()[0] if get_count else None
4✔
329
        self._log(query[:255])
4✔
330
        return count
4✔
331

332
    def delete(self, verbose=True):
4✔
333
        """
334
        Deletes the contents of the table and its dependent tables, recursively.
335
        User is prompted for confirmation if config['safemode'] is set to True.
336
        """
337
        conn = self.connection
4✔
338
        already_in_transaction = conn.in_transaction
4✔
339
        safe = config['safemode']
4✔
340
        if already_in_transaction and safe:
4✔
341
            raise DataJointError('Cannot delete within a transaction in safemode. '
×
342
                                 'Set dj.config["safemode"] = False or complete the ongoing transaction first.')
343
        graph = conn.dependencies
4✔
344
        graph.load()
4✔
345
        delete_list = collections.OrderedDict(
4✔
346
            (name, _rename_map(next(iter(graph.parents(name).items()))) if name.isdigit() else FreeTable(conn, name))
347
            for name in graph.descendants(self.full_table_name))
348

349
        # construct restrictions for each relation
350
        restrict_by_me = set()
4✔
351
        # restrictions: Or-Lists of restriction conditions for each table.
352
        # Uncharacteristically of Or-Lists, an empty entry denotes "delete everything".
353
        restrictions = collections.defaultdict(list)
4✔
354
        # restrict by self
355
        if self.restriction:
4✔
356
            restrict_by_me.add(self.full_table_name)
4✔
357
            restrictions[self.full_table_name].append(self.restriction)  # copy own restrictions
4✔
358
        # restrict by renamed nodes
359
        restrict_by_me.update(table for table in delete_list if table.isdigit())  # restrict by all renamed nodes
4✔
360
        # restrict by secondary dependencies
361
        for table in delete_list:
4✔
362
            restrict_by_me.update(graph.children(table, primary=False))   # restrict by any non-primary dependents
4✔
363

364
        # compile restriction lists
365
        for name, table in delete_list.items():
4✔
366
            for dep in graph.children(name):
4✔
367
                # if restrict by me, then restrict by the entire relation otherwise copy restrictions
368
                restrictions[dep].extend([table] if name in restrict_by_me else restrictions[name])
4✔
369

370
        # apply restrictions
371
        for name, table in delete_list.items():
4✔
372
            if not name.isdigit() and restrictions[name]:  # do not restrict by an empty list
4✔
373
                table.restrict([
4✔
374
                    r.proj() if isinstance(r, FreeTable) else (
375
                        delete_list[r[0]].proj(**{a: b for a, b in r[1]['attr_map'].items()})
376
                        if isinstance(r, _rename_map) else r)
377
                    for r in restrictions[name]])
378
        if safe:
4✔
379
            print('About to delete:')
×
380

381
        if not already_in_transaction:
4✔
382
            self.connection.start_transaction()
4✔
383
        total = 0
4✔
384
        try:
4✔
385
            for name, table in reversed(list(delete_list.items())):
4✔
386
                if not name.isdigit():
4✔
387
                    count = table.delete_quick(get_count=True)
4✔
388
                    total += count
4✔
389
                    if (verbose or safe) and count:
4✔
390
                        print('{table}: {count} items'.format(table=name, count=count))
4✔
391
        except:
×
392
            # Delete failed, perhaps due to insufficient privileges. Cancel transaction.
393
            if not already_in_transaction:
×
394
                self.connection.cancel_transaction()
×
395
            raise
×
396
        else:
397
            assert not (already_in_transaction and safe)
4✔
398
            if not total:
4✔
399
                print('Nothing to delete')
4✔
400
                if not already_in_transaction:
4✔
401
                    self.connection.cancel_transaction()
4✔
402
            else:
403
                if already_in_transaction:
4✔
404
                    if verbose:
×
405
                        print('The delete is pending within the ongoing transaction.')
×
406
                else:
407
                    if not safe or user_choice("Proceed?", default='no') == 'yes':
4✔
408
                        self.connection.commit_transaction()
4✔
409
                        if verbose or safe:
4✔
410
                            print('Committed.')
4✔
411
                    else:
412
                        self.connection.cancel_transaction()
×
413
                        if verbose or safe:
×
414
                            print('Cancelled deletes.')
×
415

416
    def drop_quick(self):
4✔
417
        """
418
        Drops the table associated with this relation without cascading and without user prompt.
419
        If the table has any dependent table(s), this call will fail with an error.
420
        """
421
        if self.is_declared:
4✔
422
            query = 'DROP TABLE %s' % self.full_table_name
4✔
423
            self.connection.query(query)
4✔
424
            logger.info("Dropped table %s" % self.full_table_name)
4✔
425
            self._log(query[:255])
4✔
426
        else:
427
            logger.info("Nothing to drop: table %s is not declared" % self.full_table_name)
×
428

429
    def drop(self):
4✔
430
        """
431
        Drop the table and all tables that reference it, recursively.
432
        User is prompted for confirmation if config['safemode'] is set to True.
433
        """
434
        if self.restriction:
4✔
435
            raise DataJointError('A relation with an applied restriction condition cannot be dropped.'
×
436
                                 ' Call drop() on the unrestricted Table.')
437
        self.connection.dependencies.load()
4✔
438
        do_drop = True
4✔
439
        tables = [table for table in self.connection.dependencies.descendants(self.full_table_name)
4✔
440
                  if not table.isdigit()]
441
        if config['safemode']:
4✔
442
            for table in tables:
4✔
443
                print(table, '(%d tuples)' % len(FreeTable(self.connection, table)))
4✔
444
            do_drop = user_choice("Proceed?", default='no') == 'yes'
4✔
445
        if do_drop:
4✔
446
            for table in reversed(tables):
4✔
447
                FreeTable(self.connection, table).drop_quick()
4✔
448
            print('Tables dropped.  Restart kernel.')
4✔
449

450
    @property
4✔
451
    def size_on_disk(self):
452
        """
453
        :return: size of data and indices in bytes on the storage device
454
        """
455
        ret = self.connection.query(
4✔
456
            'SHOW TABLE STATUS FROM `{database}` WHERE NAME="{table}"'.format(
457
                database=self.database, table=self.table_name), as_dict=True).fetchone()
458
        return ret['Data_length'] + ret['Index_length']
4✔
459

460
    def show_definition(self):
4✔
461
        raise AttributeError('show_definition is deprecated. Use the describe method instead.')
×
462

463
    def describe(self, context=None, printout=True):
4✔
464
        """
465
        :return:  the definition string for the relation using DataJoint DDL.
466
            This does not yet work for aliased foreign keys.
467
        """
468
        if context is None:
4✔
469
            frame = inspect.currentframe().f_back
4✔
470
            context = dict(frame.f_globals, **frame.f_locals)
4✔
471
            del frame
4✔
472
        if self.full_table_name not in self.connection.dependencies:
4✔
473
            self.connection.dependencies.load()
×
474
        parents = self.parents()
4✔
475
        in_key = True
4✔
476
        definition = ('# ' + self.heading.table_info['comment'] + '\n'
4✔
477
                      if self.heading.table_info['comment'] else '')
478
        attributes_thus_far = set()
4✔
479
        attributes_declared = set()
4✔
480
        indexes = self.heading.indexes.copy()
4✔
481
        for attr in self.heading.attributes.values():
4✔
482
            if in_key and not attr.in_key:
4✔
483
                definition += '---\n'
4✔
484
                in_key = False
4✔
485
            attributes_thus_far.add(attr.name)
4✔
486
            do_include = True
4✔
487
            for parent_name, fk_props in list(parents.items()):  # need list() to force a copy
4✔
488
                if attr.name in fk_props['attr_map']:
4✔
489
                    do_include = False
4✔
490
                    if attributes_thus_far.issuperset(fk_props['attr_map']):
4✔
491
                        parents.pop(parent_name)
4✔
492
                        # foreign key properties
493
                        try:
4✔
494
                            index_props = indexes.pop(tuple(fk_props['attr_map']))
4✔
495
                        except KeyError:
4✔
496
                            index_props = ''
4✔
497
                        else:
498
                            index_props = [k for k, v in index_props.items() if v]
4✔
499
                            index_props = ' [{}]'.format(', '.join(index_props)) if index_props else ''
4✔
500

501
                        if not parent_name.isdigit():
4✔
502
                            # simple foreign key
503
                            definition += '->{props} {class_name}\n'.format(
4✔
504
                                props=index_props,
505
                                class_name=lookup_class_name(parent_name, context) or parent_name)
506
                        else:
507
                            # projected foreign key
508
                            parent_name = list(self.connection.dependencies.in_edges(parent_name))[0][0]
4✔
509
                            lst = [(attr, ref) for attr, ref in fk_props['attr_map'].items() if ref != attr]
4✔
510
                            definition += '->{props} {class_name}.proj({proj_list})\n'.format(
4✔
511
                                attr_list=', '.join(r[0] for r in lst),
512
                                props=index_props,
513
                                class_name=lookup_class_name(parent_name, context) or parent_name,
514
                                proj_list=','.join('{}="{}"'.format(a,b) for a, b in lst))
515
                            attributes_declared.update(fk_props['attr_map'])
4✔
516
            if do_include:
4✔
517
                attributes_declared.add(attr.name)
4✔
518
                definition += '%-20s : %-28s %s\n' % (
4✔
519
                    attr.name if attr.default is None else '%s=%s' % (attr.name, attr.default),
520
                    '%s%s' % (attr.type, ' auto_increment' if attr.autoincrement else ''),
521
                    '# ' + attr.comment if attr.comment else '')
522
        # add remaining indexes
523
        for k, v in indexes.items():
4✔
524
            definition += '{unique}INDEX ({attrs})\n'.format(
4✔
525
                unique='UNIQUE ' if v['unique'] else '',
526
                attrs=', '.join(k))
527
        if printout:
4✔
528
            print(definition)
4✔
529
        return definition
4✔
530

531
    def _update(self, attrname, value=None):
4✔
532
        """
533
            Updates a field in an existing tuple. This is not a datajoyous operation and should not be used
534
            routinely. Relational database maintain referential integrity on the level of a tuple. Therefore,
535
            the UPDATE operator can violate referential integrity. The datajoyous way to update information is
536
            to delete the entire tuple and insert the entire update tuple.
537

538
            Safety constraints:
539
               1. self must be restricted to exactly one tuple
540
               2. the update attribute must not be in primary key
541

542
            Example
543

544
            >>> (v2p.Mice() & key).update('mouse_dob',   '2011-01-01')
545
            >>> (v2p.Mice() & key).update( 'lens')   # set the value to NULL
546

547
        """
548
        if len(self) != 1:
4✔
549
            raise DataJointError('Update is only allowed on one tuple at a time')
4✔
550
        if attrname not in self.heading:
4✔
551
            raise DataJointError('Invalid attribute name')
×
552
        if attrname in self.heading.primary_key:
4✔
553
            raise DataJointError('Cannot update a key value.')
×
554

555
        attr = self.heading[attrname]
4✔
556

557
        if attr.is_blob:
4✔
558
            value = blob.pack(value)
4✔
559
            placeholder = '%s'
4✔
560
        elif attr.numeric:
4✔
561
            if value is None or np.isnan(np.float(value)):  # nans are turned into NULLs
4✔
562
                placeholder = 'NULL'
4✔
563
                value = None
4✔
564
            else:
565
                placeholder = '%s'
4✔
566
                value = str(int(value) if isinstance(value, bool) else value)
4✔
567
        else:
568
            placeholder = '%s'
4✔
569
        command = "UPDATE {full_table_name} SET `{attrname}`={placeholder} {where_clause}".format(
4✔
570
            full_table_name=self.from_clause,
571
            attrname=attrname,
572
            placeholder=placeholder,
573
            where_clause=self.where_clause)
574
        self.connection.query(command, args=(value, ) if value is not None else ())
4✔
575

576

577
def lookup_class_name(name, context, depth=3):
4✔
578
    """
579
    given a table name in the form `schema_name`.`table_name`, find its class in the context.
580
    :param name: `schema_name`.`table_name`
581
    :param context: dictionary representing the namespace
582
    :param depth: search depth into imported modules, helps avoid infinite recursion.
583
    :return: class name found in the context or None if not found
584
    """
585
    # breadth-first search
586
    nodes = [dict(context=context, context_name='', depth=depth)]
4✔
587
    while nodes:
4✔
588
        node = nodes.pop(0)
4✔
589
        for member_name, member in node['context'].items():
4✔
590
            if not member_name.startswith('_'):  # skip IPython's implicit variables
4✔
591
                if inspect.isclass(member) and issubclass(member, Table):
4✔
592
                    if member.full_table_name == name:   # found it!
4✔
593
                        return '.'.join([node['context_name'],  member_name]).lstrip('.')
4✔
594
                    try:  # look for part tables
4✔
595
                        parts = member._ordered_class_members
4✔
596
                    except AttributeError:
×
597
                        pass  # not a UserTable -- cannot have part tables.
×
598
                    else:
599
                        for part in (getattr(member, p) for p in parts if p[0].isupper() and hasattr(member, p)):
4✔
600
                            if inspect.isclass(part) and issubclass(part, Table) and part.full_table_name == name:
4✔
601
                                return '.'.join([node['context_name'], member_name, part.__name__]).lstrip('.')
4✔
602
                elif node['depth'] > 0 and inspect.ismodule(member) and member.__name__ != 'datajoint':
4✔
603
                    try:
4✔
604
                        nodes.append(
4✔
605
                            dict(context=dict(inspect.getmembers(member)),
606
                                 context_name=node['context_name'] + '.' + member_name,
607
                                 depth=node['depth']-1))
608
                    except ImportError:
×
609
                        pass  # could not import, so do not attempt
×
610
    return None
4✔
611

612

613
class FreeTable(Table):
4✔
614
    """
615
    A base relation without a dedicated class. Each instance is associated with a table
616
    specified by full_table_name.
617
    :param arg:  a dj.Connection or a dj.FreeTable
618
    """
619

620
    def __init__(self, arg, full_table_name=None):
4✔
621
        super().__init__()
4✔
622
        if isinstance(arg, FreeTable):
4✔
623
            # copy constructor
624
            self.database = arg.database
4✔
625
            self._table_name = arg._table_name
4✔
626
            self._connection = arg._connection
4✔
627
        else:
628
            self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.'))
4✔
629
            self._connection = arg
4✔
630

631
    def __repr__(self):
4✔
632
        return "FreeTable(`%s`.`%s`)" % (self.database, self._table_name)
4✔
633

634
    @property
4✔
635
    def table_name(self):
636
        """
637
        :return: the table name in the schema
638
        """
639
        return self._table_name
4✔
640

641

642
class Log(Table):
4✔
643
    """
644
    The log table for each schema.
645
    Instances are callable.  Calls log the time and identifying information along with the event.
646
    """
647

648
    def __init__(self, arg, database=None):
4✔
649
        super().__init__()
4✔
650

651
        if isinstance(arg, Log):
4✔
652
            # copy constructor
653
            self.database = arg.database
4✔
654
            self._connection = arg._connection
4✔
655
            self._definition = arg._definition
4✔
656
            self._user = arg._user
4✔
657
            return
4✔
658

659
        self.database = database
4✔
660
        self._connection = arg
4✔
661
        self._definition = """    # event logging table for `{database}`
4✔
662
        timestamp = CURRENT_TIMESTAMP : timestamp
663
        ---
664
        version  :varchar(12)   # datajoint version
665
        user     :varchar(255)  # user@host
666
        host=""  :varchar(255)  # system hostname
667
        event="" :varchar(255)  # custom message
668
        """.format(database=database)
669

670
        if not self.is_declared:
4✔
671
            self.declare()
4✔
672
        self._user = self.connection.get_user()
4✔
673

674
    @property
4✔
675
    def definition(self):
676
        return self._definition
4✔
677

678
    @property
4✔
679
    def table_name(self):
680
        return '~log'
4✔
681

682
    def __call__(self, event):
4✔
683
        try:
4✔
684
            self.insert1(dict(
4✔
685
                user=self._user,
686
                version=version + 'py',
687
                host=platform.uname().node,
688
                event=event), skip_duplicates=True, ignore_extra_fields=True)
689
        except DataJointError:
4✔
690
            logger.info('could not log event in table ~log')
4✔
691

692
    def delete(self):
4✔
693
        """bypass interactive prompts and cascading dependencies"""
694
        self.delete_quick()
×
695

696
    def drop(self):
4✔
697
        """bypass interactive prompts and cascading dependencies"""
698
        self.drop_quick()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc