Releases: DerTraurigeHund/DynamicModel
Release list
v0.3-alpha
Changelog
Version: v0.3-alpha
Datum: 2025-08-12
Hinweis: Baseline war v0.2-alpha (ganz am Anfang dieses Chats). Diese Version (v0.3-alpha) fasst alle Änderungen, Erweiterungen und Verbesserungen zusammen, die seit v0.2-alpha implementiert wurden.
Übersicht (Kurz)
v0.3-alpha erweitert das Mini‑ORM erheblich: Connection‑Management & Pooling, robustere Transaktionen mit Savepoints, persistente Migrations‑Historie, Audit‑Trigger, umfangreiche DDL- und Batch‑Methoden, Soft‑Delete‑Integration in Queries, Hooks erweitert, Schema‑Caching, Typ‑Inference, Streaming für große Abfragen, Logging und viele Convenience‑Methoden (get_by, get_or_create, paginate_with_count, usw.).
Neu (Added)
-
Connection / Pooling
- connect_pool(minconn, maxconn, **db_params)
- close() / close_pool()
-
Logging & Observability
- set_logger(fn) zum Registrieren eines Query‑Loggers
- healthcheck() für einfache DB‑Erreichbarkeitsprüfung
- explain(query, analyze=True) zur Abfrageoptimierung
-
Transaktionen & Savepoints
- transaction() Contextmanager mit Verschachtelung (innere Ebenen verwenden Savepoints)
- savepoint(name=None) separater Savepoint‑Context
-
Migrations‑Framework (persistente Historie)
- schema_migrations Tabelle wird angelegt
- run_migrations() protokolliert ausgeführte Migrationen
- add_migration(name, fn) zur Registrierung
-
Audit‑Trail
- enable_audit_trail(table, audit_table="audit_log") legt Trigger und Audit‑Tabelle an
-
Hooks
- Global und tabellenspezifische BEFORE/AFTER Hooks (register_before_insert, register_after_insert)
- BEFORE‑Hooks können Daten mutieren; AFTER‑Hooks werden exceptionsicher ausgeführt
-
Soft‑Delete
- soft_delete(), restore_soft_deleted(), purge_soft_deleted_older_than()
- automatische Berücksichtigung von deleted-Spalte in Abfragen (exclude_deleted flag)
-
CRUD / Convenience
- get_by(), first(), last(), get_or_create()
- paginate_with_count()
- list_all_ids()
- exists_by_id()
- to_dict(), refresh(), clone_row(), copy_row_to_table()
-
Upsert / Bulk / Batch
- upsert(table, conflict_cols, values, update_cols=None) (RETURNING id)
- bulk_create(rows) (execute_values)
- bulk_update(rows, key="id") via VALUES/... FROM‑Konstrukt
- execute_batch(query, params, page_size) (psycopg2.extras.execute_batch)
-
Streaming & große Datenmengen
- stream_query(query, params=(), fetch_size=1000) mit serverseitigem Cursor
-
Schema / DDL
- inspect_schema(table) mit TTL‑Cache (set_schema_cache_ttl)
- ensure_columns(table, columns: Dict[col, SQLType]) für Mehrfach‑ALTER
- create_table, drop_table, add_index, add_unique, add_foreign_key, drop_constraint, rename_column, drop_column
- add_timestamps(table) + Trigger für updated_at
- ensure_version_column(table, version_col="version") für Optimistic Locking
- vacuum_analyze(table=None)
-
Optimistic Locking
- save_with_version(version_col="version") mit vorherigem ensure_version_column
-
Aggregates
- aggregate(table, func, column, **conditions) (SUM/MIN/MAX/AVG/COUNT etc.)
-
Schema‑Caching und Typ‑Inference
- automatisches Anlegen neuer Spalten mit inferiertem SQL‑Typ (TEXT, BOOLEAN, BIGINT, JSONB, TIMESTAMP, ...)
- Cache default TTL = 300s, konfigurierbar via set_schema_cache_ttl
Verbesserungen (Changed)
-
_get_cursor() / Connection-Handling
- Unterstützung für Pooling vs. Einzelverbindung, Cursor‑Commit/Rollback jetzt korrekt mit/ohne outer transaction
- Cursor wird immer geschlossen; Pool‑Verbindung wird nur zurückgegeben, wenn nicht in globaler transaction()
-
transaction()
- Verschachtelung mittels Savepoints (innere Scope → SAVEPOINT / RELEASE / ROLLBACK TO)
- threadlocal Speicherung der Verbindung/Depth (Thread‑sicherer Kontext)
-
Hooks
- Unterstützung globaler ("*") Hooks neben tabellenspezifischen Hooks
- BEFORE‑Hooks werfen Fehler (abbruch); AFTER‑Hooks werden log‑sicher ausgeführt (Fehler werden unterdrückt)
-
Schema‑Inspektion/Cache
- TTL‑Cache zur Verminderung von information_schema‑Abfragen, Cache‑Invalidierung nach DDL‑Operationen
-
Soft‑Delete
- Automatische Filterung (exclude_deleted=True) in find_ids/get_all/paginate/count/exists
-
create(), bulk_create(), upsert()
- Optional: Spaltentypen explizit per column_types oder infer_types=True
- Fehlende Spalten werden automatisch mit sinnvoller Typwahl angelegt
-
Fehlerbehandlung
- Logging des SQL (wenn Logger gesetzt) ohne das Ausführen zu beeinflussen
- Hooks und Logger dürfen nicht die DB‑Operationen destabilisieren
Fehlerbehebungen (Fixed)
- Resource‑Leaks beim Pooling verhindert (Cursor schließen, putconn sicherstellen)
- Transaktionslogik: commit/rollback wird korrekt nur am Outermost‑Level durchgeführt
- Bulk‑Inserts: execute_values korrekt eingesetzt und RETURNING id gelesen
- Schema‑Änderungen invalidieren nun Cache korrekt
Entfernt / Deprecated
- Keine Methoden wurden absichtlich entfernt; einige Signaturen wurden erweitert (kompatibel, aber erweiterte Parameter vorhanden):
- create(...) Signature erweitert um column_types & infer_types
- register_before_insert/register_after_insert nehmen jetzt Optional table (None/"*" = global)
Breaking Changes / Migration Notes
- Wenn du externe Code hast, der register_before_insert(table, fn) mit table None benutzt hat, das Verhalten ist jetzt konsistent: verwende "*" oder None für globale Hooks.
- create() akzeptiert jetzt zusätzliche optionale Parameter (column_types, infer_types) — bestehende Aufrufe bleiben kompatibel.
- transaction(): vorherige Implementierung kann in seltenen Fällen ein anderes Verhalten beim nested transaction handling gehabt haben; überprüfe komplexe Migrationsskripts, die sich auf implicit commits verlassen.
- run_migrations() legt nun persistente Tabelle schema_migrations an. Falls du bereits eine Tabelle mit anderem Schema namens schema_migrations hast, müssten Konflikte überprüft werden.
- add_timestamps() und enable_audit_trail() legen PL/pgSQL‑Funktionen/Trigger an — falls Namen kollidieren, passe Namen an.
Empfehlung vor Upgrade:
- Backup der DB / Schema.
- Prüfe vorhandene Tabellen
schema_migrations/audit_logauf Namenskonflikte. - Führe run_migrations() in Testumgebung aus.
- Falls du save_with_version einsetzen willst: zuerst ensure_version_column(table).
Upgrade‑/Migrations‑Beispiel
-
Verbindung herstellen (Test):
- DM.connect_pool(...)
-
Logging einschalten (optional):
- DM.set_logger(lambda q,p: print(q, p))
-
Migrationen ausführen:
- DM.add_migration("001_add_index_users_email", lambda: DM.add_index("users","email", unique=True))
- DM.run_migrations()
-
Audit aktivieren (optional):
- DM.enable_audit_trail("users")
-
Versionierung (optional):
- DM.ensure_version_column("users")
Beispiele (neues Verhalten)
- get_or_create:
obj, created = DM.get_or_create("users", defaults={"name":"X"}, email="a@b.c")- Upsert:
uid = DM.upsert("users", conflict_cols=["email"], values={"email":"a@b.c","name":"Ada"})- Transaction mit Savepoint:
with DM.transaction():
a = DM.create("t", x=1)
with DM.savepoint():
b = DM.create("t", x=2)
# Fehler -> nur Savepoint rollback- Streaming:
for row in DM.stream_query("SELECT * FROM big_table"):
process(row)Sonstiges / Hinweise
- Default Schema‑Cache TTL = 300 Sekunden (konfigurierbar)
- Typ‑Inference ist heuristisch; bei Bedarf explizit column_types übergeben
- Alle neu hinzugefügten Features sind auf SQL‑Injection geschützt (sql.Identifier, Parameterbindung). Achte dennoch bei dynamischen SQL‑Fragments (z. B. aggregate func) auf Validierung.
v0.2-alpha
Neu:
• bulk_create(table, list_of_dicts)
Ermöglicht das Einfügen vieler Datensätze in einem einzigen INSERT, statt jede Zeile einzeln zu schreiben.
• update_by_conditions(table, updates: dict, **conditions)
Führt ein UPDATE auf alle Zeilen aus, die den angegebenen Bedingungen genügen (ähnlich zu find_ids, aber mit SET).
• delete_by_conditions(table, **conditions)
Löscht alle Zeilen anhand von WHERE-Kriterien, ohne sie vorher als Objekte zu instanziieren.
• get_all(table, **conditions) → List[DynamicModel]
Liefert direkt eine Liste von Model-Instanzen mit optionalen Filterkriterien, z. B. get_all("users", active="true").
• paginate(table, page: int, per_page: int, **conditions)
Kombiniert WHERE, LIMIT und OFFSET und gibt die IDs oder Instanzen zurück.
• count(table, *conditions) → int
Zählt Zeilen (SELECT COUNT()…), optional mit WHERE-Bedingungen.
• exists(table, **conditions) → bool
Prüft, ob mindestens eine Zeile die Bedingungen erfüllt.
• order_by(table, *columns, asc=True, **conditions)
Erweitert get_all oder find_ids um eine ORDER-BY-Klausel.
• transaction(ctx_callable)
Erlaubt es, mehrere Änderungen in einer einzigen Transaktion zusammenzufassen (mit Rollback bei Fehler).
• raw_query(sql_string, params)
Macht eigene, frei formatierte Abfragen möglich, wenn das ORM mal nicht ausreicht.
• add_index(table, column: str, unique=False)
Legt einen Index oder Unique-Constraint an, um Abfragen zu beschleunigen.
• drop_column(table, column: str)
Entfernt eine Spalte wieder (gegenüber dem bereits vorhandenen ADD COLUMN).
• rename_column(table, old: str, new: str)
Erlaubt das Umbenennen von Spalten.
• inspect_schema(table) → Dict[str, dict]
Gibt zu jeder Spalte Metadaten zurück (Typ, Default, nullable etc.), um Validierungen vor der Laufzeit zu ermöglichen.
• relationship(one: str, many: str, fk: str)
Definiert Verknüpfungen zwischen Tabellen („Has-Many“ / „Belongs-To“) und sorgt für automatisches Join-Fetching.
• before_insert / after_insert hooks
Callback-Mechanismus, um z. B. automatisch Timestamps zu setzen oder Eingaben zu validieren.
• soft_delete(table, **conditions)
Setzt statt Löschen ein „deleted_at“-Feld oder Boolean-Flag, um Datenbank-Historie zu bewahren.
• schema_migration(tooling)
Ermöglicht versionierte Migrationen (Up / Down), um Spalten hinzuzufügen/zu ändern/anpassen.
• connection_pooling(minconn, maxconn)
Verwaltet einen Pool von Verbindungen anstelle einer Einzelverbindung, um Performance in Mehrbenutzer-Szenarien zu optimieren.
• support für andere Dialekte (SQLite, MySQL)
Abstrahiert SQL-Dialekt-Unterschiede, damit das Tool nicht nur auf Postgres limitiert ist.
v0.1-alpha
DynamicModel
DynamicModel ist eine kleine Python-ORM-ähnliche Hilfsklasse für PostgreSQL (via psycopg2), mit der du
- eine Verbindung aufbauen
- Tabellen dynamisch anlegen
- neue Datensätze erstellen (Spalten werden bei Bedarf automatisch als
TEXTergänzt) - anhand von Spaltenwerten alle passenden
ids finden - dynamisch auf Spalten zugreifen und neue Spalten per Attributszuweisung erstellen können
Inhalt
- Installation
- Verbindung aufbauen
- Tabelle anlegen
- Datensatz erstellen
- IDs finden
- Mit Datensätzen arbeiten
- API-Reference
Installation
Stelle sicher, dass du psycopg2 installiert hast:
pip install psycopg2Dann importiere die Klasse:
from dynamic_model import DynamicModel(oder füge den Code direkt in dein Projekt ein)
Verbindung aufbauen
Bevor du irgendetwas mit der Datenbank machst, musst du die Verbindung konfigurieren:
DynamicModel.connect(
dbname="meine_db",
user="postgres",
password="geheim",
host="localhost",
port=5432
)Ab jetzt steht DynamicModel._connection allen Methoden zur Verfügung.
Tabelle anlegen
Eine neue Tabelle erzeugst du mit einem Namenspaar und einem Schema-Dictionary:
DynamicModel.create_table(
"users",
{
"username": "TEXT NOT NULL UNIQUE",
"email": "TEXT",
"active": "BOOLEAN DEFAULT TRUE"
}
)Intern wird automatisch eine Spalte
id SERIAL PRIMARY KEYhinzugefügt.
Datensatz erstellen
Um einen neuen Datensatz einzufügen:
user = DynamicModel.create(
"users",
username="homer",
email="homer@springfield.com",
active="true"
)
print("Neue ID:", user._id) # z.B. 1
print("Username:", user.username) # "homer"Fehlende Spalten im Schema werden automatisch als TEXT hinzugefügt.
IDs finden
Mit find_ids() kannst du per AND-Verknüpfung nach IDs suchen:
# alle aktiven User
ids_active = DynamicModel.find_ids("users", active="true")
# User mit username='homer' und active='true'
ids_homer = DynamicModel.find_ids("users",
username="homer",
active="true")Die Methode gibt eine Liste von Ganzzahlen zurück.
Mit Datensätzen arbeiten
Nach dem create() oder wenn du ein existierendes Objekt lädst…
user = DynamicModel("users", 1)kannst du:
- Ein vorhandenes Feld lesen:
print(user.email)
- Ein vorhandenes Feld schreiben (speichert sofort in der DB):
user.email = "homer@newmail.com"
- Ein neues Feld per Attribut anlegen (legt automatisch eine
TEXT-Spalte an und schreibt den Wert):user.favourite_donut = "Glazed" print(user.favourite_donut)
Möchtest du mehrere Änderungen gesammelt speichern, kannst du save() aufrufen:
user.username = "homer_simpson"
user.active = "false"
user.save()API Reference
connect(**db_params)
Richtet die globale DB-Verbindung ein.
Parameter
dbname(str)user(str)password(str)host(str)port(int)
DynamicModel.connect(
dbname="db",
user="usr",
password="pw",
host="localhost",
port=5432
)create_table(table: str, schema: dict)
Erzeugt eine neue Tabelle (falls nicht existiert).
table: Tabellennameschema: Dict aus Spaltenname → SQL-Typ
DynamicModel.create_table(
"products",
{"name":"TEXT", "price":"NUMERIC"}
)create(table: str, **kwargs) → DynamicModel
Fügt einen Datensatz ein. Fehlende Spalten werden als TEXT erstellt.
Gibt das neue Objekt zurück.
p = DynamicModel.create("products",
name="Donut",
price="1.50")find_ids(table: str, **conditions) → List[int]
Sucht in table alle Zeilen, die alle Bedingungen erfüllen.
Rückgabe: Liste der id-Werte.
ids = DynamicModel.find_ids("users",
active="true",
email="homer@springfield.com")init(table: str, row_id: int)
Lädt eine bestehende Zeile (Spalten + Werte) aus der DB.
user = DynamicModel("users", 1)getattr(name)
Gibt den Wert der Spalte name zurück, falls vorhanden, sonst AttributeError.
setattr(name, value)
- Wenn
nameeine existierende Spalte ist, wird sofort einUPDATEausgeführt. - Andernfalls wird eine neue
TEXT-Spalte angelegt und befüllt.
save()
Speichert alle Spalten (außer id) im Batch. Nützlich, wenn _data manuell verändert wurde.
user.username = "bart"
user.email = "bart@simpsons.com"
# …
user.save()Komplettes Beispiel
from dynamic_model import DynamicModel
# 1) connect
DynamicModel.connect(dbname="demo",
user="postgres",
password="geheim",
host="localhost",
port=5432)
# 2) Tabelle anlegen
DynamicModel.create_table("users", {
"username":"TEXT NOT NULL UNIQUE",
"email": "TEXT",
"active": "BOOLEAN DEFAULT TRUE"
})
# 3) Datensätze erstellen
DynamicModel.create("users",
username="alice",
email="alice@example.com",
active="true")
DynamicModel.create("users",
username="bob",
email="bob@example.com",
active="false")
# 4) IDs finden
print(DynamicModel.find_ids("users", active="true")) # z.B. [1]
# 5) Objekt laden & dynamisch arbeiten
u = DynamicModel("users", 1)
print(u.username) # "alice"
u.nickname = "Allie" # neue Spalte + Wert
print(u.nickname) # "Allie"
# 6) Mehr Änderungen & batch-save
u.email = "alice@new.com"
u.active = "false"
u.save()