dirty: sql stuff
This commit is contained in:
parent
cc4b567181
commit
156af70b6e
3 changed files with 147 additions and 0 deletions
0
src/byteb4rb1e/utils/dataclasses/__init__.py
Normal file
0
src/byteb4rb1e/utils/dataclasses/__init__.py
Normal file
147
src/byteb4rb1e/utils/dataclasses/sql.py
Normal file
147
src/byteb4rb1e/utils/dataclasses/sql.py
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
from dataclasses import dataclass, fields, Field
|
||||||
|
import datetime
|
||||||
|
from typing import Optional, get_origin, get_args, Union, List
|
||||||
|
|
||||||
|
|
||||||
|
def escape_value(value: str) -> str:
|
||||||
|
"""Escapes a string value for safe SQL insertion.
|
||||||
|
|
||||||
|
:param value: Raw string value
|
||||||
|
|
||||||
|
:return: Escaped SQL-safe string
|
||||||
|
"""
|
||||||
|
return "'" + str(value).replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_fields_from_expr(expr: Expr) -> List[str]:
|
||||||
|
"""Recursively extracts field names used in an expression tree.
|
||||||
|
|
||||||
|
:param expr: Expression object
|
||||||
|
|
||||||
|
:return: List of field names referenced in the expression
|
||||||
|
"""
|
||||||
|
if isinstance(expr, (Eq, Gt, Lt, Ne, Like)):
|
||||||
|
return [expr.field]
|
||||||
|
elif isinstance(expr, (And, Or)):
|
||||||
|
fields = []
|
||||||
|
for sub in expr.conditions:
|
||||||
|
fields.extend(extract_fields_from_expr(sub))
|
||||||
|
return fields
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(freeze=True)
|
||||||
|
class GIDModelProps:
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
table_prefix: Optional[str] = None,
|
||||||
|
pk_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GIDModelParent:
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
table_name: str
|
||||||
|
foreign_key_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GIDModel:
|
||||||
|
"""Base class for SQL-aware dataclasses with support for schema generation,
|
||||||
|
insertion, and query generation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_table_sql(
|
||||||
|
cls: "GIDModel",
|
||||||
|
props: GIDModelProps,
|
||||||
|
parent: Optional[GIDModelParent] = None,
|
||||||
|
tables: Optional[[str, GIDModel]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Generates SQL CREATE TABLE statements for the model and its nested
|
||||||
|
components.
|
||||||
|
|
||||||
|
:param table_prefix: Optional prefix for table names
|
||||||
|
:param root_table: Name of the root table for foreign key references
|
||||||
|
|
||||||
|
:return: SQL CREATE TABLE statement(s)
|
||||||
|
"""
|
||||||
|
tables = tables or []
|
||||||
|
|
||||||
|
table_name = f"{{props.table_prefix} or ''}{cls.__name__.lower()}"
|
||||||
|
|
||||||
|
if table_name in tables.keys():
|
||||||
|
if not cls == tables[table_name]:
|
||||||
|
raise Exception('table with name '{table_name}' already incorporated with different schema')
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
columns = ["{props.pk_name} TEXT PRIMARY KEY"] if props.pk_name else []
|
||||||
|
|
||||||
|
nested_sql = []
|
||||||
|
foreign_keys = []
|
||||||
|
|
||||||
|
if parent and parent.table_name != table_name:
|
||||||
|
foreign_keys.append(
|
||||||
|
f"FOREIGN KEY (gid) REFERENCES {parent.table_name}({parent.foreign_key_name}) "
|
||||||
|
f"ON DELETE CASCADE ON UPDATE CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
for f in fields(cls):
|
||||||
|
if f.name == props.pk_name: continue # already handled
|
||||||
|
|
||||||
|
py_type = f.type
|
||||||
|
|
||||||
|
|
||||||
|
if get_origin(py_type) is Union and get_args(py_type)[1] is type(None):
|
||||||
|
py_type = get_args(py_type)[0]
|
||||||
|
|
||||||
|
sql_type = SQL_TYPECAST.get(py_type)
|
||||||
|
|
||||||
|
metadata = f.metadata.get("gid", None)
|
||||||
|
|
||||||
|
if issubclass(py_type, List):
|
||||||
|
py_type = get_args(py_type)[0]
|
||||||
|
pk_name = None
|
||||||
|
parent_fk_name = f.name
|
||||||
|
else:
|
||||||
|
pk_name = 'id'
|
||||||
|
parent_fk_name = f.name
|
||||||
|
|
||||||
|
if issubclass(py_type, GIDModel):
|
||||||
|
if props.pk_name:
|
||||||
|
raise Exception('gidless model with nesting not possible')
|
||||||
|
nested_table = f"{props.table_prefix}{py_type.__name__.lower()}"
|
||||||
|
|
||||||
|
# determine whether to pass parent or self
|
||||||
|
# if no primary key, pass the parent if it exists
|
||||||
|
if not props.pk_name:
|
||||||
|
f_parent=parent or None
|
||||||
|
# if primary, pass self
|
||||||
|
else:
|
||||||
|
f_parent=GIDModelParent(
|
||||||
|
table_name=table,
|
||||||
|
fk_name=f.name
|
||||||
|
)
|
||||||
|
|
||||||
|
nested_sql.append(
|
||||||
|
py_type.create_table_sql(
|
||||||
|
props=f_props or GIDModelProps(
|
||||||
|
table_name=f.name
|
||||||
|
pk_name=f_parent.fk_name
|
||||||
|
)
|
||||||
|
parent=f_parent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if sql_type:
|
||||||
|
column_def = f"{f.name} {sql_type}"
|
||||||
|
if metadata:
|
||||||
|
column_def += f" {metadata}"
|
||||||
|
columns.append(column_def)
|
||||||
|
else:
|
||||||
|
raise Exception(f"Unable to typecast field '{f.name}' with type '{py_type}' to SQL")
|
||||||
|
|
||||||
|
all_defs = columns + foreign_keys
|
||||||
|
main_sql = f"CREATE TABLE {table} (\n {',\n '.join(all_defs)}\n);"
|
||||||
|
return "\n".join([main_sql] + nested_sql)
|
||||||
|
|
||||||
0
tests/unit/byteb4rb1e/utils/dataclasses/__init__.py
Normal file
0
tests/unit/byteb4rb1e/utils/dataclasses/__init__.py
Normal file
Loading…
Add table
Add a link
Reference in a new issue