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
52 changes: 33 additions & 19 deletions src/dbjavagenix/ai/naming_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from ..core.java_identifiers import to_pascal_case


COMMON_PREFIXES = (
"sys_", "biz_", "bd_", "cms_", "ops_", "admin_", "core_",
"t_", "tb_", # 通用建表习惯
"sys_",
"biz_",
"bd_",
"cms_",
"ops_",
"admin_",
"core_",
"t_",
"tb_", # 通用建表习惯
)

# 不规则单数化(主要场景,不追求完全覆盖)
Expand Down Expand Up @@ -102,7 +111,9 @@ def infer_business_name(
# 2. 检测关联表: ≥ 2 个外键且其他列很少
if _is_association_table(columns, foreign_keys):
# 关联表命名: 用两个外键的 referenced_table 组合
ref_tables = [_strip_known_prefix(fk.get("references_table", "")) for fk in foreign_keys[:2]]
ref_tables = [
_strip_known_prefix(fk.get("references_table", "")) for fk in foreign_keys[:2]
]
ref_classes = [_to_pascal_case(_singularize(rt)) for rt in ref_tables if rt]
if len(ref_classes) >= 2:
class_name = f"{ref_classes[0]}{ref_classes[1]}Assignment"
Expand Down Expand Up @@ -178,21 +189,27 @@ def _is_association_table(
pk_count = sum(1 for c in columns if c.get("primary_key"))
# 业务列 = 非主键 + 非外键
business_cols = [
c for c in columns
if not c.get("primary_key") and c.get("name") not in fk_cols
c for c in columns if not c.get("primary_key") and c.get("name") not in fk_cols
]
# 时间戳/审计字段不算业务列
business_cols = [
c for c in business_cols
if not _is_audit_column(c.get("name", ""))
]
business_cols = [c for c in business_cols if not _is_audit_column(c.get("name", ""))]
# 业务列 ≤ 1 (例如 'remark') 视为关联表
return len(business_cols) <= 1 and pk_count <= 1


_AUDIT_PATTERNS = ("created_at", "updated_at", "create_time", "update_time",
"created_by", "updated_by", "deleted_at", "deleted",
"version", "tenant_id", "remark")
_AUDIT_PATTERNS = (
"created_at",
"updated_at",
"create_time",
"update_time",
"created_by",
"updated_by",
"deleted_at",
"deleted",
"version",
"tenant_id",
"remark",
)


def _is_audit_column(name: str) -> bool:
Expand All @@ -202,7 +219,7 @@ def _is_audit_column(name: str) -> bool:
def _strip_known_prefix(name: str) -> str:
for prefix in COMMON_PREFIXES:
if name.startswith(prefix):
return name[len(prefix):]
return name[len(prefix) :]
return name


Expand All @@ -219,7 +236,7 @@ def _singularize(name: str) -> str:
# ses / xes / ches / shes
for suf in ("ses", "xes", "ches", "shes"):
if lower.endswith(suf):
return lower[: -2]
return lower[:-2]
# 复数 s 去掉 (排除 ss / us / is 等不应该去的)
if lower.endswith("s") and not lower.endswith(("ss", "us", "is", "as", "os")):
return lower[:-1]
Expand All @@ -230,18 +247,15 @@ def _singularize(name: str) -> str:


def _to_pascal_case(name: str) -> str:
parts = [p for p in _CAMEL_SPLIT_RE.split(name) if p]
return "".join(p[:1].upper() + p[1:] for p in parts)
return to_pascal_case(name)


def _tokenize(name: str) -> List[str]:
"""把下划线/连字符分隔的名字拆为 lowercase 词列表"""
return [p.lower() for p in _CAMEL_SPLIT_RE.split(name) if p]


def infer_business_names_batch(
tables: List[Dict[str, Any]]
) -> List[NamingInference]:
def infer_business_names_batch(tables: List[Dict[str, Any]]) -> List[NamingInference]:
"""对一批表批量推断 (输入格式与 LLM 模式一致)"""
return [
infer_business_name(
Expand Down
132 changes: 132 additions & 0 deletions src/dbjavagenix/core/java_identifiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Deterministic Java identifier normalisation shared by code-generation paths."""

from __future__ import annotations

import re


# Java keywords, literals and contextual keywords that are unsafe as generated names.
JAVA_KEYWORDS = frozenset(
{
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extends",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true",
"false",
"null",
"_",
# Contextual keywords used by modern Java language features.
"module",
"open",
"opens",
"provides",
"requires",
"to",
"transitive",
"uses",
"with",
"yield",
"record",
"sealed",
"permits",
"non-sealed",
"var",
"when",
}
)

_CAMEL_BOUNDARY_RE = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")


def _words(value: str) -> list[str]:
"""Split a database name into alphanumeric words while retaining Unicode letters."""
if not isinstance(value, str):
return []

# Treat every non-alphanumeric character as a separator. This keeps the original
# database identifier untouched while ensuring generated Java names are portable.
normalized = "".join(char if (char == "_" or char.isalnum()) else "_" for char in value)
words: list[str] = []
for part in normalized.split("_"):
words.extend(piece for piece in _CAMEL_BOUNDARY_RE.split(part) if piece)
return words


def _fallback_name(value: str) -> str:
return "" if value == "" else "Generated"


def to_pascal_case(value: str) -> str:
"""Convert a database identifier to a valid Java-style PascalCase name."""
words = _words(value)
if not words:
return _fallback_name(value)

result = "".join(word[:1].upper() + word[1:].lower() for word in words)
if result and result[0].isdigit():
result = f"Generated{result}"
return result or _fallback_name(value)


def to_camel_case(value: str) -> str:
"""Convert a database identifier to a valid Java-style camelCase name."""
pascal = to_pascal_case(value)
if not pascal:
return ""

result = pascal[:1].lower() + pascal[1:]
if result in JAVA_KEYWORDS:
result = f"{result}_"
return result


def is_valid_java_identifier(value: str) -> bool:
"""Return whether *value* is a Java identifier safe for generated source."""
return bool(value) and value != "_" and value.isidentifier() and value not in JAVA_KEYWORDS
5 changes: 3 additions & 2 deletions src/dbjavagenix/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pydantic import BaseModel, Field
from urllib.parse import quote

from .java_identifiers import to_pascal_case


class DatabaseType(str, Enum):
"""Supported database types"""
Expand Down Expand Up @@ -88,8 +90,7 @@ def entity_name(self) -> str:
@staticmethod
def _to_pascal_case(snake_str: str) -> str:
"""Convert snake_case to PascalCase"""
components = snake_str.split("_")
return "".join(word.capitalize() for word in components)
return to_pascal_case(snake_str)


class DatabaseConfig(BaseModel):
Expand Down
Loading
Loading