sqlnotify 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. sqlnotify-0.1.0/LICENSE +21 -0
  2. sqlnotify-0.1.0/PKG-INFO +610 -0
  3. sqlnotify-0.1.0/README.md +574 -0
  4. sqlnotify-0.1.0/pyproject.toml +122 -0
  5. sqlnotify-0.1.0/setup.cfg +4 -0
  6. sqlnotify-0.1.0/src/sqlnotify/__init__.py +14 -0
  7. sqlnotify-0.1.0/src/sqlnotify/adapters/__init__.py +0 -0
  8. sqlnotify-0.1.0/src/sqlnotify/adapters/asgi.py +40 -0
  9. sqlnotify-0.1.0/src/sqlnotify/constants.py +9 -0
  10. sqlnotify-0.1.0/src/sqlnotify/dialects/__init__.py +12 -0
  11. sqlnotify-0.1.0/src/sqlnotify/dialects/base.py +183 -0
  12. sqlnotify-0.1.0/src/sqlnotify/dialects/postgresql.py +778 -0
  13. sqlnotify-0.1.0/src/sqlnotify/dialects/sqlite.py +797 -0
  14. sqlnotify-0.1.0/src/sqlnotify/dialects/utils.py +74 -0
  15. sqlnotify-0.1.0/src/sqlnotify/exceptions.py +38 -0
  16. sqlnotify-0.1.0/src/sqlnotify/logger.py +32 -0
  17. sqlnotify-0.1.0/src/sqlnotify/notifiers/__init__.py +3 -0
  18. sqlnotify-0.1.0/src/sqlnotify/notifiers/base.py +240 -0
  19. sqlnotify-0.1.0/src/sqlnotify/notifiers/notifier.py +639 -0
  20. sqlnotify-0.1.0/src/sqlnotify/types.py +57 -0
  21. sqlnotify-0.1.0/src/sqlnotify/utils.py +165 -0
  22. sqlnotify-0.1.0/src/sqlnotify/watcher.py +72 -0
  23. sqlnotify-0.1.0/src/sqlnotify.egg-info/PKG-INFO +610 -0
  24. sqlnotify-0.1.0/src/sqlnotify.egg-info/SOURCES.txt +33 -0
  25. sqlnotify-0.1.0/src/sqlnotify.egg-info/dependency_links.txt +1 -0
  26. sqlnotify-0.1.0/src/sqlnotify.egg-info/entry_points.txt +2 -0
  27. sqlnotify-0.1.0/src/sqlnotify.egg-info/requires.txt +8 -0
  28. sqlnotify-0.1.0/src/sqlnotify.egg-info/top_level.txt +1 -0
  29. sqlnotify-0.1.0/tests/test_postgres_dialect.py +417 -0
  30. sqlnotify-0.1.0/tests/test_postgres_notifier.py +383 -0
  31. sqlnotify-0.1.0/tests/test_postgres_notifier_asgi_adapter.py +233 -0
  32. sqlnotify-0.1.0/tests/test_sqlite_dialect.py +304 -0
  33. sqlnotify-0.1.0/tests/test_sqlite_notifier.py +369 -0
  34. sqlnotify-0.1.0/tests/test_sqlite_notifier_asgi_adapter.py +202 -0
  35. sqlnotify-0.1.0/tests/test_watcher.py +588 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Brai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,610 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlnotify
3
+ Version: 0.1.0
4
+ Summary: A near real-time SQL notification library for database changes, supporting PostgreSQL and SQLite.
5
+ Author-email: Daniel Brai <danielbrai.dev@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Daniel-Brai/SQLNotify
8
+ Project-URL: Repository, https://github.com/Daniel-Brai/SQLNotify
9
+ Project-URL: Source, https://github.com/Daniel-Brai/SQLNotify
10
+ Project-URL: Issues, https://github.com/Daniel-Brai/SQLNotify/issues
11
+ Keywords: sql,database,notification,asyncio,postgresql,sqlite,sqlalchemy
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Framework :: AsyncIO
23
+ Classifier: Topic :: Database
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: sqlalchemy>=2.0.0
30
+ Requires-Dist: asyncpg>=0.31.0
31
+ Provides-Extra: all
32
+ Requires-Dist: aiosqlite>=0.22.1; extra == "all"
33
+ Provides-Extra: sqlite
34
+ Requires-Dist: aiosqlite>=0.22.1; extra == "sqlite"
35
+ Dynamic: license-file
36
+
37
+ # SQLNotify
38
+
39
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
40
+ [![Build and Test SQLNotify](https://github.com/Daniel-Brai/SQLNotify/actions/workflows/ci.yml/badge.svg)](https://github.com/Daniel-Brai/SQLNotify/actions/workflows/ci.yml)
41
+ [![codecov](https://codecov.io/gh/Daniel-Brai/SQLNotify/branch/main/graph/badge.svg)](https://codecov.io/gh/Daniel-Brai/SQLNotify)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
+
44
+ **React to database changes in near real-time.**
45
+
46
+ SQLNotify leverages database native notification mechanisms (like PostgreSQL's LISTEN/NOTIFY) to provide instant notifications when your database records change. It supports FastAPI, Starlette, and other ASGI frameworks.
47
+
48
+ ## Motivation for SQLNotify?
49
+
50
+ I started SQLNotify as an experiment for my projects that require real-time updates based on database changes. I wanted a solution that was simple to integrate, efficient, and didn't require external dependencies like message queues or change data capture tools.
51
+
52
+ SQLNotify uses the underling database system to push notifications the moment data changes. This enables:
53
+
54
+ - **Near real-time updates** - React to changes almost instantly
55
+ - **Lower database load** - No repeated SELECT queries checking for changes
56
+ - **Simplified architecture** - No need for message queues or external pub/sub systems
57
+ - **Type-safe** - Full typing support with SQLAlchemy 2.0+ with support for SQLModel models too
58
+ - **Production-ready** - It handles large payloads with overflow tables and automatic cleanup
59
+ - **Extensible** - Pluggable dialect system allows support for different databases
60
+
61
+ ## Installation
62
+
63
+ By default, sqlnotify install with PostgreSQL support:
64
+
65
+ ```bash
66
+ pip install sqlnotify
67
+ ```
68
+
69
+ With SQLite support:
70
+
71
+ ```bash
72
+ pip install "sqlnotify[sqlite]"
73
+ ```
74
+
75
+ With SQLModel support and all database drivers supported by SQLNotify:
76
+
77
+ ```bash
78
+ pip install "sqlnotify[all]"
79
+ ```
80
+
81
+ Using other package managers:
82
+
83
+ ```bash
84
+ # Using uv
85
+ uv add sqlnotify
86
+ # With SQLite support
87
+ uv add "sqlnotify[sqlite]"
88
+
89
+ # Using poetry
90
+ poetry add sqlnotify
91
+ # With SQLite support
92
+ poetry add "sqlnotify[sqlite]"
93
+ ```
94
+
95
+ ## Quick Start
96
+
97
+ ### Basic Usage with FastAPI
98
+
99
+ ```python
100
+ from fastapi import FastAPI
101
+ from sqlalchemy.ext.asyncio import create_async_engine
102
+ from sqlmodel import SQLModel, Field
103
+ from sqlnotify import Notifier, Operation, ChangeEvent
104
+ from sqlnotify.adapters.asgi import sqlnotify_lifespan
105
+ from contextlib import asynccontextmanager
106
+
107
+ # Define your models
108
+ class User(SQLModel, table=True):
109
+ id: int | None = Field(default=None, primary_key=True)
110
+ email: str
111
+ name: str
112
+
113
+ # Create async or syncengine
114
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
115
+ # engine = create_async_engine("sqlite+aiosqlite:///./myapp.db")
116
+
117
+ # Initialize notifier
118
+ notifier = Notifier(db_engine=engine)
119
+
120
+ # Register watchers for models and operations
121
+ # By default `extra_columns` is None and only primary key(s) (e.g., `id`) are returned in events.
122
+ notifier.watch(User, Operation.INSERT)
123
+ notifier.watch(User, Operation.UPDATE, extra_columns=["email"])
124
+
125
+ # Subscribe to changes
126
+ @notifier.subscribe(User, Operation.INSERT)
127
+ async def on_user_created(event: ChangeEvent):
128
+ print(f"New user created: {event.id}")
129
+
130
+ @notifier.subscribe(User, Operation.UPDATE)
131
+ # or @notifier.subscribe("User", Operation.UPDATE)
132
+ async def on_user_updated(event: ChangeEvent):
133
+ print(f"User {event.id} updated")
134
+
135
+ # You can also filter by specific column values
136
+ @notifier.subscribe(User, Operation.UPDATE, filters=[{"column": "id", "value": 42}])
137
+ async def on_specific_user_updated(event: ChangeEvent):
138
+ print(f"User 42 was updated")
139
+
140
+ # Setup lifespan
141
+ @asynccontextmanager
142
+ async def lifespan(app: FastAPI):
143
+ async with sqlnotify_lifespan(notifier):
144
+ yield
145
+
146
+ app = FastAPI(lifespan=lifespan)
147
+
148
+ @app.post("/users/")
149
+ async def create_user(email: str, name: str):
150
+ # Your user creation logic
151
+ # Notification will fire automatically when database triggers
152
+ return {"email": email, "name": name}
153
+ ```
154
+
155
+ ### Without ASGI Lifespan Management
156
+
157
+ ```python
158
+ from sqlalchemy import create_engine
159
+ from sqlnotify import Notifier, Operation
160
+
161
+ engine = create_engine("postgresql+psycopg2://user:pass@localhost/db")
162
+
163
+ notifier = Notifier(db_engine=engine)
164
+
165
+ # Register watchers
166
+ notifier.watch(User, Operation.INSERT, extra_columns=["email", "name"])
167
+
168
+ @notifier.subscribe(User, Operation.INSERT)
169
+ def on_user_created(event: ChangeEvent):
170
+ print(f"User {event.id} created")
171
+
172
+ # Start/stop manually (sync engine)
173
+ notifier.start()
174
+ # ... your application logic ...
175
+ notifier.stop()
176
+
177
+ # For async engine, use async methods:
178
+ # await notifier.astart()
179
+ # ... your application logic ...
180
+ # await notifier.astop()
181
+ ```
182
+
183
+ ### Using SQLite
184
+
185
+ See [SQLITE_QUICKSTART.md](docs/SQLITE_QUICKSTART.md) for the SQLite quick start guide.
186
+
187
+ ## Features
188
+
189
+ ### Pluggable Dialect System
190
+
191
+ SQLNotify uses a dialect-based architecture to support different databases. The dialect is automatically detected from your SQLAlchemy engine:
192
+
193
+ ```python
194
+ from sqlalchemy.ext.asyncio import create_async_engine
195
+ from sqlnotify import Notifier
196
+
197
+ # PostgreSQL dialect is automatically selected
198
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
199
+ notifier = Notifier(db_engine=engine)
200
+
201
+ # Access dialect information
202
+ print(notifier.dialect_name) # "postgresql"
203
+ ```
204
+
205
+ **Currently supported dialects:**
206
+
207
+ - **PostgreSQL** - Uses native LISTEN/NOTIFY mechanism (instant, <10ms latency)
208
+ - **SQLite** - Uses hybrid polling and in-memory queue (20-50ms latency, adaptive CPU usage)
209
+
210
+ ### Automatic Trigger Management
211
+
212
+ SQLNotify automatically creates and manages database triggers for your models based on operations you want to listen for:
213
+
214
+ ```python
215
+ notifier = Notifier(db_engine=engine)
216
+
217
+ # Register watchers for different models and operations
218
+ notifier.watch(User, Operation.INSERT, extra_columns=["email", "name"])
219
+ notifier.watch(User, Operation.UPDATE, extra_columns=["email"])
220
+ notifier.watch(Post, Operation.INSERT, extra_columns=["title"])
221
+
222
+ # No extra columns, just the primary key column which is usually "id"
223
+ notifier.watch(Comment, Operation.INSERT)
224
+ # if your model has a different primary key column name, specify it with primary_keys parameter
225
+ # notifier.watch(Comment, Operation.INSERT, primary_keys=["comment_id"])
226
+ # as so the `ChangeEvent.id` will be the value of the `comment_id` column instead of `id`
227
+ ```
228
+
229
+ SQLNotify triggers are created on startup and optionally cleaned up when the notifier stops.
230
+
231
+ ### Watch Specific Columns
232
+
233
+ Monitor only specific columns for changes:
234
+
235
+ ```python
236
+ notifier.watch(
237
+ User,
238
+ Operation.UPDATE,
239
+ extra_columns=["email", "name"],
240
+ trigger_columns=["email"] # Only trigger on email changes
241
+ )
242
+
243
+ @notifier.subscribe(User, Operation.UPDATE)
244
+ async def on_email_changed(event: ChangeEvent):
245
+ new_email = event.extra_columns.get("email")
246
+ print(f"User {event.id} changed email to {new_email}")
247
+ ```
248
+
249
+ ### Custom Primary Keys
250
+
251
+ Specify custom primary key column names (useful for composite keys or non-standard primary keys):
252
+
253
+ ```python
254
+ # Single primary key with custom name
255
+ notifier.watch(
256
+ User,
257
+ Operation.INSERT,
258
+ extra_columns=["email"],
259
+ primary_keys=["user_id"] # Custom primary key column
260
+ )
261
+
262
+ # Composite primary key
263
+ notifier.watch(
264
+ OrderItem,
265
+ Operation.UPDATE,
266
+ extra_columns=["quantity"],
267
+ primary_keys=["order_id", "item_id"] # Composite primary key
268
+ )
269
+
270
+ @notifier.subscribe(OrderItem, Operation.UPDATE)
271
+ async def on_order_item_updated(event: ChangeEvent):
272
+ order_id, item_id = event.id
273
+ print(f"Order item ({order_id}, {item_id}) updated")
274
+
275
+ # Empty extra_columns - only primary key(s) will be in the payload
276
+ notifier.watch(
277
+ User,
278
+ Operation.DELETE,
279
+ extra_columns=None, # No extra columns needed which is the default
280
+ primary_keys=["id"]
281
+ )
282
+ ```
283
+
284
+ **Note**:
285
+
286
+ - The default is `primary_keys=["id"]`
287
+ - For single primary keys, `event.id` is the value directly
288
+ - For composite primary keys, `event.id` is a tuple of values in the order specified
289
+ - **All specified `primary_keys` must be actual primary key columns on the model** - the system validates this at runtime
290
+ - All columns in `extra_columns`, `trigger_columns`, and `primary_keys` are validated to ensure they exist on the model
291
+ - `extra_columns` can be an empty list if you only need the primary key(s)
292
+
293
+ ### Watch Specific Records
294
+
295
+ Subscribe to changes for specific record IDs or column values:
296
+
297
+ ```python
298
+ # Filter by ID
299
+ @notifier.subscribe(User, Operation.UPDATE, filters=[{"column": "id", "value": 42}])
300
+ async def on_vip_user_updated(event: ChangeEvent):
301
+ print(f"VIP user {event.id} was updated")
302
+
303
+ # Filter by multiple columns
304
+ @notifier.subscribe(
305
+ User,
306
+ Operation.UPDATE,
307
+ filters=[
308
+ {"column": "status", "value": "active"},
309
+ {"column": "role", "value": "admin"}
310
+ ]
311
+ )
312
+ async def on_active_admin_updated(event: ChangeEvent):
313
+ print(f"Active admin user {event.id} was updated")
314
+ ```
315
+
316
+ ### Overflow Tables for Large Payloads
317
+
318
+ SQLNotify uses overflow tables for large payloads. SQLNotify handles this automatically if you enable overflow tables per watcher basis:
319
+
320
+ ```python
321
+ notifier.watch(
322
+ User,
323
+ Operation.INSERT,
324
+ extra_columns=["email", "name", "bio", "preferences"],
325
+ use_overflow_table=True # Large payloads stored in overflow table
326
+ )
327
+ ```
328
+
329
+ ### Notifications
330
+
331
+ Send custom notifications through the same channel system:
332
+
333
+ ```python
334
+ # Notify with a model instance (async engine)
335
+ await notifier.anotify(
336
+ User,
337
+ Operation.UPDATE,
338
+ payload={"id": 123, "email": "user@example.com"}
339
+ )
340
+
341
+ # Notify with custom channel label
342
+ await notifier.anotify(
343
+ "User",
344
+ Operation.INSERT,
345
+ payload={"id": 456},
346
+ channel_label="custom_channel"
347
+ )
348
+
349
+ # Notify with large payload using overflow table
350
+ # This automatically handles payloads larger than 7999 bytes
351
+ await notifier.anotify(
352
+ User,
353
+ Operation.INSERT,
354
+ payload=large_payload_dict,
355
+ use_overflow_table=True
356
+ )
357
+
358
+ # For sync engines, use notify() instead:
359
+ # notifier.notify(User, Operation.UPDATE, payload={"id": 123})
360
+ ```
361
+
362
+ **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding NOTIFY limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
363
+
364
+ ### Model Change Detection
365
+
366
+ Automatically recreate triggers when model schemas change:
367
+
368
+ ```python
369
+ notifier = Notifier(
370
+ db_engine=engine,
371
+ revoke_on_model_change=True # Drop and recreate triggers on schema changes
372
+ )
373
+
374
+ # Register watchers
375
+ # By default `extra_columns` is None and only primary key(s) (e.g., `id`) are returned in events.
376
+ notifier.watch(User, Operation.INSERT)
377
+ ```
378
+
379
+ ### Custom Channel Labels
380
+
381
+ Use custom channel names for logical grouping:
382
+
383
+ ```python
384
+ notifier.watch(
385
+ User,
386
+ Operation.INSERT,
387
+ extra_columns=["email"],
388
+ channel_label="new_registrations"
389
+ )
390
+
391
+ @notifier.subscribe("User", Operation.INSERT, channel_label="new_registrations")
392
+ async def on_new_registration(event: ChangeEvent):
393
+ print("New user registered")
394
+ ```
395
+
396
+ ## Real-World Example: WebSocket Notifications
397
+
398
+ ```python
399
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
400
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
401
+ from sqlmodel import SQLModel, Field, select
402
+ from sqlnotify import Notifier, Operation, ChangeEvent
403
+ from sqlnotify.adapters.asgi import sqlnotify_lifespan
404
+ from contextlib import asynccontextmanager
405
+ from typing import List
406
+
407
+ # Models
408
+ class User(SQLModel, table=True):
409
+ id: int | None = Field(default=None, primary_key=True)
410
+ email: str
411
+ name: str
412
+
413
+ class Post(SQLModel, table=True):
414
+ id: int | None = Field(default=None, primary_key=True)
415
+ title: str
416
+ content: str
417
+ user_id: int = Field(foreign_key="user.id")
418
+
419
+ # Database setup
420
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
421
+ async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
422
+
423
+ # WebSocket connection manager
424
+ class ConnectionManager:
425
+ def __init__(self):
426
+ self.active_connections: List[WebSocket] = []
427
+
428
+ async def connect(self, websocket: WebSocket):
429
+ await websocket.accept()
430
+ self.active_connections.append(websocket)
431
+
432
+ def disconnect(self, websocket: WebSocket):
433
+ self.active_connections.remove(websocket)
434
+
435
+ async def broadcast(self, message: dict):
436
+ for connection in self.active_connections:
437
+ try:
438
+ await connection.send_json(message)
439
+ except:
440
+ pass
441
+
442
+ manager = ConnectionManager()
443
+
444
+ # Notifier setup
445
+ notifier = Notifier(db_engine=engine)
446
+
447
+ # Register watchers
448
+ notifier.watch(User, Operation.INSERT, extra_columns=["email", "name"])
449
+ notifier.watch(Post, Operation.INSERT, extra_columns=["title", "user_id"])
450
+
451
+ @notifier.subscribe(User, Operation.INSERT)
452
+ async def broadcast_new_user(event: ChangeEvent):
453
+ await manager.broadcast({
454
+ "type": "user_created",
455
+ "id": event.id,
456
+ "email": event.extra_columns.get("email"),
457
+ "name": event.extra_columns.get("name")
458
+ })
459
+
460
+ @notifier.subscribe(Post, Operation.INSERT)
461
+ async def broadcast_new_post(event: ChangeEvent):
462
+ await manager.broadcast({
463
+ "type": "post_created",
464
+ "id": event.id,
465
+ "title": event.extra_columns.get("title"),
466
+ "user_id": event.extra_columns.get("user_id")
467
+ })
468
+
469
+ @notifier.subscribe(User, Operation.UPDATE)
470
+ async def broadcast_user_update(event: ChangeEvent):
471
+ await manager.broadcast({
472
+ "type": "user_updated",
473
+ "id": event.id
474
+ })
475
+
476
+ # Lifespan
477
+ @asynccontextmanager
478
+ async def lifespan(app: FastAPI):
479
+ async with sqlnotify_lifespan(notifier):
480
+ yield
481
+
482
+ app = FastAPI(lifespan=lifespan)
483
+
484
+ # WebSocket endpoint
485
+ @app.websocket("/ws")
486
+ async def websocket_endpoint(websocket: WebSocket):
487
+ await manager.connect(websocket)
488
+ async for _ in websocket.iter_text():
489
+ pass # Just keep connection alive for broadcasts
490
+ manager.disconnect(websocket)
491
+
492
+ # API endpoints
493
+ @app.post("/users/")
494
+ async def create_user(email: str, name: str):
495
+ async with async_session_maker() as session:
496
+ user = User(email=email, name=name)
497
+ session.add(user)
498
+ await session.commit()
499
+ await session.refresh(user)
500
+ return user
501
+
502
+ @app.post("/posts/")
503
+ async def create_post(title: str, content: str, user_id: int):
504
+ async with async_session_maker() as session:
505
+ post = Post(title=title, content=content, user_id=user_id)
506
+ session.add(post)
507
+ await session.commit()
508
+ await session.refresh(post)
509
+ return post
510
+ ```
511
+
512
+ ## Configuration
513
+
514
+ ```python
515
+ notifier = Notifier(
516
+ db_engine=engine, # Required: AsyncEngine or Engine
517
+ revoke_on_model_change=True, # Drop and recreate triggers when model schema changes
518
+ cleanup_on_start=False, # Remove existing triggers/functions on startup
519
+ use_logger=True # Enable internal logging
520
+ )
521
+ ```
522
+
523
+ **Note**: Models and operations are registered individually using `notifier.watch()` method.
524
+
525
+ ### Watch Method Options
526
+
527
+ ```python
528
+ notifier.watch(
529
+ model=User, # Model class to watch
530
+ operation=Operation.UPDATE, # INSERT, UPDATE, or DELETE
531
+ extra_columns=["email", "name"], # Columns to include in notifications (can be empty list)
532
+ trigger_columns=["email"], # Only trigger on these column changes (UPDATE only)
533
+ primary_keys=["id"], # Primary key columns (must be actual PKs on model)
534
+ channel_label="custom_name", # Custom channel identifier
535
+ use_overflow_table=False # Store large payloads and process them using the overflow table
536
+ )
537
+ ```
538
+
539
+ ## Database Support
540
+
541
+ SQLNotify uses a pluggable dialect system for database support:
542
+
543
+ | Database | Dialect | Status | Mechanism | Latency |
544
+ |----------|---------|--------|-----------|----------|
545
+ | PostgreSQL 9.0+ | `postgresql` | ✅ Supported | LISTEN/NOTIFY | <10ms |
546
+ | SQLite 3.0+ | `sqlite` | ✅ Supported | Hybrid Polling and In-Memory Queue | 20-50ms |
547
+ | MySQL | `mysql` | 🔜 Planned | - | - |
548
+
549
+ ### When to Use SQLite
550
+
551
+ ✅ **Use SQLite when:**
552
+
553
+ - When you are building small to medium applications
554
+ - You are running in single-process or embedded environments
555
+ - You need a lightweight, serverless database
556
+ - You want a simple setup without external dependencies or separate database servers
557
+ - You can tolerate ~20-50ms notification latency
558
+
559
+ ❌ **Don't use SQLite when:**
560
+
561
+ - Building large scale distributed systems
562
+ - You are running in multi-process or multi-server architecture
563
+ - You need almost instant notifications (<10ms latency)
564
+ - You need very high throughput requirements (>5000 msgs/sec) and cross server communication
565
+
566
+ ## Framework Support
567
+
568
+ SQLNotify works with any ASGI framework:
569
+
570
+ - **FastAPI** - Use `sqlnotify_lifespan` helper
571
+ - **Starlette** - Use `sqlnotify_lifespan` helper
572
+ - **Other ASGI frameworks** - Implement lifespan management manually
573
+
574
+ For synchronous frameworks, use sync mode:
575
+
576
+ ```python
577
+ notifier = Notifier(db_engine=engine, ...)
578
+ notifier.start() # Synchronous start
579
+ ```
580
+
581
+ ## How It Works
582
+
583
+ 1. **Dialect Detection** - SQLNotify automatically detects the database dialect from your SQLAlchemy engine
584
+ 2. **Trigger Creation** - The dialect creates database-specific triggers on your tables
585
+ 3. **Change Detection** - When data changes, triggers fire and call notification functions
586
+ 4. **NOTIFY** - Functions use database-native notification mechanisms (e.g., PostgreSQL's NOTIFY)
587
+ 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications
588
+ 6. **Event Distribution** - Incoming notifications are routed to subscribed callbacks
589
+ 7. **Callback Execution** - Your subscriber functions are called with change events
590
+
591
+ ## Performance Considerations
592
+
593
+ - **Dedicated Connection** - SQLNotify uses a separate connection for LISTEN to avoid blocking if the engine is PostgreSQL
594
+ - **Async by Default** - Built for asyncio, but supports sync mode when needed
595
+ - **Overflow Handling** - Large payloads automatically routed to overflow tables
596
+ - **Automatic Cleanup** - Consumed overflow records are cleaned up after 1 hour
597
+ - **Minimal Overhead** - Triggers are efficient as such notification delivery is near instant
598
+
599
+ ## Contributing
600
+
601
+ I welcome any contribution. Please see the [Contributing Guide](CONTRIBUTING.md) for details.
602
+
603
+ ## License
604
+
605
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
606
+
607
+ ## Acknowledgments
608
+
609
+ - Built with [SQLAlchemy](https://www.sqlalchemy.org/)
610
+ - PostgreSQL [LISTEN/NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) documentation