Skip to content
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
8 changes: 8 additions & 0 deletions src/datajoint/declare.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,14 @@ def compile_attribute(
DataJointError
If syntax is invalid, primary key is nullable, or blob has invalid default.
"""
if line.lstrip().startswith("_"):
raise DataJointError(
f'Attribute name in line "{line}" starts with an underscore. '
"Names with leading underscore are reserved for platform-managed "
"columns (e.g. _job_start_time, _singleton). Use a regular "
"attribute name; if you need to control visibility at the call "
"site, use proj()."
)
try:
match = attribute_parser.parse_string(line + "#", parse_all=True)
except pp.ParseException as err:
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_declare_hidden_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Unit tests for the leading-underscore guard in attribute declarations.

Regression coverage for issue #1433: declarations like ``_hidden: bool``
previously failed with a cryptic ``pyparsing.ParseException``. The framework
intentionally does not support user-defined hidden attributes — those names
are reserved for platform-managed columns (e.g. ``_job_start_time``,
``_singleton``) which DataJoint injects programmatically after parsing.

This test ensures the user gets a clear ``DataJointError`` pointing to the
right alternative, not a parser-internals error.
"""

import pytest

from datajoint.declare import attribute_parser, compile_attribute
from datajoint.errors import DataJointError


@pytest.mark.parametrize(
"line",
[
"_hidden: bool",
"_params_hash: varchar(32)",
" _leading_whitespace: int32",
],
)
def test_compile_attribute_rejects_leading_underscore(line):
"""The leading-underscore guard fires before the parser, so adapter is unused."""
with pytest.raises(DataJointError, match="reserved for platform-managed"):
compile_attribute(line, in_key=False, foreign_key_sql=[], context={}, adapter=None)


def test_parser_still_rejects_leading_underscore():
"""Parser regex itself remains strict; the helpful error fires before the parser."""
import pyparsing as pp

with pytest.raises(pp.ParseException):
attribute_parser.parse_string("_hidden: bool#", parse_all=True)


def test_parser_still_accepts_plain_names():
match = attribute_parser.parse_string("name: varchar(40)#", parse_all=True)
assert match["name"] == "name"


def test_parser_rejects_digit_start():
"""Numeric leading char remains invalid (preserved behavior)."""
import pyparsing as pp

with pytest.raises(pp.ParseException):
attribute_parser.parse_string("1bad: int32#", parse_all=True)
Loading