Skip to content

Codegen: add ql.db_table_name property pragma #19063

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

Merged
merged 14 commits into from
Mar 31, 2025
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
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ build --java_language_version=17
build --tool_java_language_version=17
build --tool_java_runtime_version=remotejdk_17
build --java_runtime_version=remotejdk_17
build --@rules_python//python/config_settings:python_version=3.12

try-import %workspace%/local.bazelrc
2 changes: 2 additions & 0 deletions .bazelrc.internal
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ common --registry=https://bcr.bazel.build
# its implementation packages without providing any code itself.
# We either can depend on internal implementation details, or turn of strict deps.
common --@rules_dotnet//dotnet/settings:strict_deps=false

build --@rules_python//python/config_settings:python_version=3.12
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ use_repo(csharp_main_extension, "paket.main")
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
hub_name = "codegen_deps",
python_version = "3.11",
python_version = "3.12",
requirements_lock = "//misc/codegen:requirements_lock.txt",
)
use_repo(pip, "codegen_deps")
Expand Down
2 changes: 1 addition & 1 deletion misc/codegen/.python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11
3.12
19 changes: 13 additions & 6 deletions misc/codegen/generators/cppgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,27 @@ def _get_type(t: str, add_or_none_except: typing.Optional[str] = None) -> str:
return t


def _get_trap_name(cls: schema.Class, p: schema.Property) -> str | None:
if p.is_single:
return None
overridden_trap_name = p.pragmas.get("ql_db_table_name")
if overridden_trap_name:
return inflection.camelize(overridden_trap_name)
trap_name = inflection.camelize(f"{cls.name}_{p.name}")
if p.is_predicate:
return trap_name
return inflection.pluralize(trap_name)


def _get_field(cls: schema.Class, p: schema.Property, add_or_none_except: typing.Optional[str] = None) -> cpp.Field:
trap_name = None
if not p.is_single:
trap_name = inflection.camelize(f"{cls.name}_{p.name}")
if not p.is_predicate:
trap_name = inflection.pluralize(trap_name)
args = dict(
field_name=p.name + ("_" if p.name in cpp.cpp_keywords else ""),
base_type=_get_type(p.type, add_or_none_except),
is_optional=p.is_optional,
is_repeated=p.is_repeated,
is_predicate=p.is_predicate,
is_unordered=p.is_unordered,
trap_name=trap_name,
trap_name=_get_trap_name(cls, p),
)
args.update(cpp.get_field_override(p.name))
return cpp.Field(**args)
Expand Down
25 changes: 21 additions & 4 deletions misc/codegen/generators/dbschemegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
log = logging.getLogger(__name__)


class Error(Exception):
pass


def dbtype(typename: str, add_or_none_except: typing.Optional[str] = None) -> str:
""" translate a type to a dbscheme counterpart, using `@lower_underscore` format for classes.
For class types, appends an underscore followed by `null` if provided
Expand Down Expand Up @@ -65,11 +69,12 @@ def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], a
)
# use property-specific tables for 1-to-many and 1-to-at-most-1 properties
for f in cls.properties:
overridden_table_name = f.pragmas.get("ql_db_table_name")
if f.synth:
continue
if f.is_unordered:
yield Table(
name=inflection.tableize(f"{cls.name}_{f.name}"),
name=overridden_table_name or inflection.tableize(f"{cls.name}_{f.name}"),
columns=[
Column("id", type=dbtype(cls.name)),
Column(inflection.singularize(f.name), dbtype(f.type, add_or_none_except)),
Expand All @@ -79,7 +84,7 @@ def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], a
elif f.is_repeated:
yield Table(
keyset=KeySet(["id", "index"]),
name=inflection.tableize(f"{cls.name}_{f.name}"),
name=overridden_table_name or inflection.tableize(f"{cls.name}_{f.name}"),
columns=[
Column("id", type=dbtype(cls.name)),
Column("index", type="int"),
Expand All @@ -90,7 +95,7 @@ def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], a
elif f.is_optional:
yield Table(
keyset=KeySet(["id"]),
name=inflection.tableize(f"{cls.name}_{f.name}"),
name=overridden_table_name or inflection.tableize(f"{cls.name}_{f.name}"),
columns=[
Column("id", type=dbtype(cls.name)),
Column(f.name, dbtype(f.type, add_or_none_except)),
Expand All @@ -100,14 +105,25 @@ def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], a
elif f.is_predicate:
yield Table(
keyset=KeySet(["id"]),
name=inflection.underscore(f"{cls.name}_{f.name}"),
name=overridden_table_name or inflection.underscore(f"{cls.name}_{f.name}"),
columns=[
Column("id", type=dbtype(cls.name)),
],
dir=dir,
)


def check_name_conflicts(decls: list[Table | Union]):
names = set()
for decl in decls:
match decl:
case Table(name=name):
if name in names:
raise Error(f"Duplicate table name: {
name}, you can use `@ql.db_table_name` on a property to resolve this")
names.add(name)


def get_declarations(data: schema.Schema):
add_or_none_except = data.root_class.name if data.null else None
declarations = [d for cls in data.classes.values() if not cls.imported for d in cls_to_dbscheme(cls,
Expand All @@ -120,6 +136,7 @@ def get_declarations(data: schema.Schema):
declarations += [
Union(dbtype(t, data.null), [dbtype(t), dbtype(data.null)]) for t in sorted(property_classes)
]
check_name_conflicts(declarations)
return declarations


Expand Down
9 changes: 6 additions & 3 deletions misc/codegen/generators/qlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
internal="ql_internal" in prop.pragmas,
)
ql_name = prop.pragmas.get("ql_name", prop.name)
db_table_name = prop.pragmas.get("ql_db_table_name")
if db_table_name and prop.is_single:
raise Error(f"`db_table_name` pragma is not supported for single properties, but {cls.name}.{prop.name} has it")
if prop.is_single:
args.update(
singular=inflection.camelize(ql_name),
Expand All @@ -141,22 +144,22 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
args.update(
singular=inflection.singularize(inflection.camelize(ql_name)),
plural=inflection.pluralize(inflection.camelize(ql_name)),
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
tablename=db_table_name or inflection.tableize(f"{cls.name}_{prop.name}"),
tableparams=["this", "index", "result"] if not prop.is_unordered else ["this", "result"],
doc=_get_doc(cls, prop, plural=False),
doc_plural=_get_doc(cls, prop, plural=True),
)
elif prop.is_optional:
args.update(
singular=inflection.camelize(ql_name),
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
tablename=db_table_name or inflection.tableize(f"{cls.name}_{prop.name}"),
tableparams=["this", "result"],
doc=_get_doc(cls, prop),
)
elif prop.is_predicate:
args.update(
singular=inflection.camelize(ql_name, uppercase_first_letter=False),
tablename=inflection.underscore(f"{cls.name}_{prop.name}"),
tablename=db_table_name or inflection.underscore(f"{cls.name}_{prop.name}"),
tableparams=["this"],
doc=_get_doc(cls, prop),
)
Expand Down
22 changes: 14 additions & 8 deletions misc/codegen/generators/rustgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,28 @@ def _get_type(t: str) -> str:
return t


def _get_table_name(cls: schema.Class, p: schema.Property) -> str:
if p.is_single:
return inflection.tableize(cls.name)
overridden_table_name = p.pragmas.get("ql_db_table_name")
if overridden_table_name:
return overridden_table_name
table_name = f"{cls.name}_{p.name}"
if p.is_predicate:
return inflection.underscore(table_name)
else:
return inflection.tableize(table_name)


def _get_field(cls: schema.Class, p: schema.Property) -> rust.Field:
table_name = inflection.tableize(cls.name)
if not p.is_single:
table_name = f"{cls.name}_{p.name}"
if p.is_predicate:
table_name = inflection.underscore(table_name)
else:
table_name = inflection.tableize(table_name)
args = dict(
field_name=rust.avoid_keywords(p.name),
base_type=_get_type(p.type),
is_optional=p.is_optional,
is_repeated=p.is_repeated,
is_predicate=p.is_predicate,
is_unordered=p.is_unordered,
table_name=table_name,
table_name=_get_table_name(cls, p),
)
args.update(rust.get_field_override(p.name))
return rust.Field(**args)
Expand Down
86 changes: 47 additions & 39 deletions misc/codegen/lib/schemadefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
Callable as _Callable,
Dict as _Dict,
Iterable as _Iterable,
ClassVar as _ClassVar,
Union as _Union,
)
from copy import deepcopy as _deepcopy
from misc.codegen.lib import schema as _schema
import inspect as _inspect
from dataclasses import dataclass as _dataclass
Expand Down Expand Up @@ -75,7 +76,7 @@ class _Namespace:
""" simple namespacing mechanism """
_name: str

def add(self, pragma: "_PragmaBase", key: str | None = None):
def add(self, pragma: _Union["_PragmaBase", "_Parametrized"], key: str | None = None):
self.__dict__[pragma.pragma] = pragma
pragma.pragma = key or f"{self._name}_{pragma.pragma}"

Expand All @@ -101,6 +102,10 @@ def negate(self) -> _schema.PropertyModifier:
@_dataclass
class _PragmaBase:
pragma: str
value: object = None

def _apply(self, pragmas: _Dict[str, object]) -> None:
pragmas[self.pragma] = self.value


@_dataclass
Expand All @@ -109,7 +114,6 @@ class _ClassPragma(_PragmaBase):
For schema classes it acts as a python decorator with `@`.
"""
inherited: bool = False
value: object = None

def __call__(self, cls: type) -> type:
""" use this pragma as a decorator on classes """
Expand All @@ -122,23 +126,19 @@ def __call__(self, cls: type) -> type:
self._apply(cls._pragmas)
return cls

def _apply(self, pragmas: _Dict[str, object]) -> None:
pragmas[self.pragma] = self.value


@_dataclass
class _Pragma(_ClassPragma, _schema.PropertyModifier):
""" A class or property pragma.
For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
For schema classes it acts as a python decorator with `@`.
class _PropertyPragma(_PragmaBase, _schema.PropertyModifier):
""" A property pragma.
It functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
"""
remove: bool = False

def modify(self, prop: _schema.Property):
self._apply(prop.pragmas)

def negate(self) -> _schema.PropertyModifier:
return _Pragma(self.pragma, remove=True)
return _PropertyPragma(self.pragma, remove=not self.remove)

def _apply(self, pragmas: _Dict[str, object]) -> None:
if self.remove:
Expand All @@ -148,31 +148,38 @@ def _apply(self, pragmas: _Dict[str, object]) -> None:


@_dataclass
class _ParametrizedClassPragma(_PragmaBase):
""" A class parametrized pragma.
Needs to be applied to a parameter to give a class pragma.
class _Pragma(_ClassPragma, _PropertyPragma):
""" A class or property pragma.
For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
For schema classes it acts as a python decorator with `@`.
"""
_pragma_class: _ClassVar[type] = _ClassPragma

inherited: bool = False
factory: _Callable[..., object] = None

def __post_init__(self):
self.__signature__ = _inspect.signature(self.factory).replace(return_annotation=self._pragma_class)
class _Parametrized[P, **Q, T]:
""" A parametrized pragma.
Needs to be applied to a parameter to give a pragma.
"""

def __init__(self, pragma_instance: P, factory: _Callable[Q, T]):
self.pragma_instance = pragma_instance
self.factory = factory
self.__signature__ = _inspect.signature(self.factory).replace(return_annotation=type(self.pragma_instance))

def __call__(self, *args, **kwargs) -> _pragma_class:
return self._pragma_class(self.pragma, self.inherited, value=self.factory(*args, **kwargs))
@property
def pragma(self):
return self.pragma_instance.pragma

@pragma.setter
def pragma(self, value):
self.pragma_instance.pragma = value

@_dataclass
class _ParametrizedPragma(_ParametrizedClassPragma):
""" A class or property parametrized pragma.
Needs to be applied to a parameter to give a pragma.
"""
_pragma_class: _ClassVar[type] = _Pragma
def __invert__(self) -> "_Parametrized[P, Q, T]":
return _Parametrized(~self.pragma_instance, factory=self.factory)

def __invert__(self) -> _Pragma:
return _Pragma(self.pragma, remove=True)
def __call__(self, *args: Q.args, **kwargs: Q.kwargs) -> T:
ret = _deepcopy(self.pragma_instance)
ret.value = self.factory(*args, **kwargs)
return ret


class _Optionalizer(_schema.PropertyModifier):
Expand Down Expand Up @@ -232,30 +239,31 @@ def __getitem__(self, item):

use_for_null = _ClassPragma("null")

qltest.add(_Pragma("skip"))
qltest.add(_ClassPragma("skip"))
qltest.add(_ClassPragma("collapse_hierarchy"))
qltest.add(_ClassPragma("uncollapse_hierarchy"))
qltest.add(_ParametrizedClassPragma("test_with", inherited=True, factory=_schema.get_type_name))
qltest.add(_Parametrized(_ClassPragma("test_with", inherited=True), factory=_schema.get_type_name))

ql.add(_ParametrizedClassPragma("default_doc_name", factory=lambda doc: doc))
ql.add(_Parametrized(_ClassPragma("default_doc_name"), factory=lambda doc: doc))
ql.add(_ClassPragma("hideable", inherited=True))
ql.add(_Pragma("internal"))
ql.add(_ParametrizedPragma("name", factory=lambda name: name))
ql.add(_Parametrized(_Pragma("name"), factory=lambda name: name))
ql.add(_Parametrized(_PropertyPragma("db_table_name"), factory=lambda name: name))

cpp.add(_Pragma("skip"))

rust.add(_Pragma("detach"))
rust.add(_PropertyPragma("detach"))
rust.add(_Pragma("skip_doc_test"))

rust.add(_ParametrizedClassPragma("doc_test_signature", factory=lambda signature: signature))
rust.add(_Parametrized(_ClassPragma("doc_test_signature"), factory=lambda signature: signature))

group = _ParametrizedClassPragma("group", inherited=True, factory=lambda group: group)
group = _Parametrized(_ClassPragma("group", inherited=True), factory=lambda group: group)


synth.add(_ParametrizedClassPragma("from_class", factory=lambda ref: _schema.SynthInfo(
synth.add(_Parametrized(_ClassPragma("from_class"), factory=lambda ref: _schema.SynthInfo(
from_class=_schema.get_type_name(ref))), key="synth")
synth.add(_ParametrizedClassPragma("on_arguments", factory=lambda **kwargs:
_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})), key="synth")
synth.add(_Parametrized(_ClassPragma("on_arguments"), factory=lambda **kwargs:
_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})), key="synth")


@_dataclass(frozen=True)
Expand Down
Loading
Loading