Skip to content

Releases: DerTraurigeHund/DynamicModel

v0.3-alpha

Choose a tag to compare

@DerTraurigeHund DerTraurigeHund released this 12 Aug 19:20
5df1fd2

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_log auf Namenskonflikte.
  • Führe run_migrations() in Testumgebung aus.
  • Falls du save_with_version einsetzen willst: zuerst ensure_version_column(table).

Upgrade‑/Migrations‑Beispiel

  1. Verbindung herstellen (Test):

    • DM.connect_pool(...)
  2. Logging einschalten (optional):

    • DM.set_logger(lambda q,p: print(q, p))
  3. Migrationen ausführen:

    • DM.add_migration("001_add_index_users_email", lambda: DM.add_index("users","email", unique=True))
    • DM.run_migrations()
  4. Audit aktivieren (optional):

    • DM.enable_audit_trail("users")
  5. 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

Choose a tag to compare

@DerTraurigeHund DerTraurigeHund released this 03 Jun 10:19
71bf4dc

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

Choose a tag to compare

@DerTraurigeHund DerTraurigeHund released this 03 Jun 10:15
2e0cd3f

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 TEXT ergänzt)
  • anhand von Spaltenwerten alle passenden ids finden
  • dynamisch auf Spalten zugreifen und neue Spalten per Attributszuweisung erstellen können

Inhalt

  1. Installation
  2. Verbindung aufbauen
  3. Tabelle anlegen
  4. Datensatz erstellen
  5. IDs finden
  6. Mit Datensätzen arbeiten
  7. API-Reference

Installation

Stelle sicher, dass du psycopg2 installiert hast:

pip install psycopg2

Dann 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 KEY

hinzugefü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: Tabellenname
  • schema: 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 name eine existierende Spalte ist, wird sofort ein UPDATE ausgefü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()