kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl

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 (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1525 @@
1
+ <!-- Database Design skill - schema design and migrations -->
2
+
3
+ database-design mode: DATA PERSISTENCE DONE RIGHT
4
+
5
+ when this skill is active, you follow disciplined database design practices.
6
+ this is a comprehensive guide to schema design and database migrations.
7
+
8
+
9
+ PHASE 0: ENVIRONMENT PREREQUISITES VERIFICATION
10
+
11
+ before designing ANY schema, verify your database environment is ready.
12
+
13
+
14
+ check database client
15
+
16
+ <terminal>psql --version 2>/dev/null || echo "postgresql client not installed"</terminal>
17
+
18
+ <terminal>sqlite3 --version 2>/dev/null || echo "sqlite3 not installed"</terminal>
19
+
20
+ <terminal>mysql --version 2>/dev/null || echo "mysql client not installed"</terminal>
21
+
22
+ install based on your database:
23
+ <terminal>brew install postgresql</terminal> # macOS
24
+ <terminal>apt-get install postgresql-client</terminal> # ubuntu
25
+ <terminal>pip install psycopg2-binary</terminal> # python driver
26
+
27
+
28
+ check orm setup
29
+
30
+ <terminal>python -c "import sqlalchemy; print(sqlalchemy.__version__)"</terminal>
31
+
32
+ if not installed:
33
+ <terminal>pip install sqlalchemy</terminal>
34
+
35
+
36
+ check alembic for migrations
37
+
38
+ <terminal>python -c "import alembic; print(alembic.__version__)"</terminal>
39
+
40
+ if not installed:
41
+ <terminal>pip install alembic</terminal>
42
+
43
+
44
+ check database connection
45
+
46
+ <terminal>cat .env 2>/dev/null | grep -i database || echo "no database config in .env"</terminal>
47
+
48
+ <terminal>echo $DATABASE_URL 2>/dev/null || echo "DATABASE_URL not set"</terminal>
49
+
50
+
51
+ verify existing migrations
52
+
53
+ <terminal>ls -la migrations/ 2>/dev/null || echo "no migrations directory"</terminal>
54
+
55
+ <terminal>ls -la alembic/versions/ 2>/dev/null || echo "no alembic versions"</terminal>
56
+
57
+
58
+ check for schema visualization tools
59
+
60
+ <terminal>which dbdiagram.io 2>/dev/null || echo "consider schema viz tools"</terminal>
61
+
62
+ <terminal>pip install eralchemy 2>/dev/null || echo "eralchemy for schema diagrams"</terminal>
63
+
64
+
65
+ PHASE 1: DATA MODELING FUNDAMENTALS
66
+
67
+
68
+ understand the domain
69
+
70
+ before touching a database, answer these questions:
71
+
72
+ [ ] what are the core entities?
73
+ [ ] what are the relationships between them?
74
+ [ ] what data needs to be persisted?
75
+ [ ] what are the query patterns?
76
+ [ ] what are the data volume estimates?
77
+ [ ] what are the consistency requirements?
78
+
79
+ domain modeling checklist:
80
+ - identify nouns as potential entities
81
+ - identify verbs as relationships
82
+ - identify adjectives as attributes
83
+ - consider the lifecycle of each entity
84
+
85
+
86
+ normalize your data
87
+
88
+ normalization eliminates redundancy and prevents anomalies:
89
+
90
+ first normal form (1NF):
91
+ - eliminate repeating groups
92
+ - each cell contains atomic values
93
+ - each record is unique
94
+
95
+ second normal form (2NF):
96
+ - must be in 1NF
97
+ - eliminate partial dependencies
98
+ - all non-key attributes depend on the entire primary key
99
+
100
+ third normal form (3NF):
101
+ - must be in 2NF
102
+ - eliminate transitive dependencies
103
+ - non-key attributes depend only on the primary key
104
+
105
+ boyce-codd normal form (BCNF):
106
+ - stronger version of 3NF
107
+ - every determinant is a candidate key
108
+
109
+
110
+ when to denormalize
111
+
112
+ denormalize for:
113
+ - read-heavy workloads
114
+ - reporting and analytics
115
+ - frequently accessed data
116
+ - reducing join complexity
117
+
118
+ denormalize sparingly and deliberately.
119
+ document every denormalization decision.
120
+
121
+
122
+ PHASE 2: IDENTIFYING ENTITIES AND ATTRIBUTES
123
+
124
+
125
+ entity definition
126
+
127
+ each entity should have:
128
+ - a clear purpose
129
+ - a primary key
130
+ - a set of attributes
131
+ - relationships to other entities
132
+
133
+ <create>
134
+ <file>src/models/user.py</file>
135
+ <content>
136
+ """User entity model."""
137
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean
138
+ from sqlalchemy.orm import declarative_base
139
+ from datetime import datetime
140
+
141
+ Base = declarative_base()
142
+
143
+
144
+ class User(Base):
145
+ """User entity representing application users."""
146
+
147
+ __tablename__ = "users"
148
+
149
+ # primary key - always required
150
+ id = Column(Integer, primary_key=True, autoincrement=True)
151
+
152
+ # required attributes
153
+ email = Column(String(255), nullable=False, unique=True, index=True)
154
+ username = Column(String(50), nullable=False, unique=True, index=True)
155
+ password_hash = Column(String(255), nullable=False)
156
+
157
+ # optional attributes
158
+ full_name = Column(String(100))
159
+ avatar_url = Column(String(500))
160
+ bio = Column(String(500))
161
+
162
+ # status fields
163
+ is_active = Column(Boolean, default=True, nullable=False)
164
+ is_verified = Column(Boolean, default=False, nullable=False)
165
+
166
+ # timestamps
167
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
168
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
169
+ last_login_at = Column(DateTime)
170
+
171
+ def __repr__(self):
172
+ return f"<User(id={self.id}, username={self.username})>"
173
+ </content>
174
+ </create>
175
+
176
+
177
+ attribute types guide
178
+
179
+ integers:
180
+ - id fields: Integer or BigInteger
181
+ - counts: Integer, default 0
182
+ - enums: Integer with check constraint
183
+
184
+ strings:
185
+ - short names: String(50-100)
186
+ - emails: String(255), add index
187
+ - urls: String(500-2000)
188
+ - rich text: Text or Text(n)
189
+
190
+ dates:
191
+ - created_at: DateTime, default=datetime.utcnow
192
+ - updated_at: DateTime, onupdate=datetime.utcnow
193
+ - dates only: Date
194
+
195
+ decimals:
196
+ - money: Numeric(10, 2) or Numeric(19, 4)
197
+ - percentages: Numeric(5, 2)
198
+
199
+ booleans:
200
+ - flags: Boolean, default=False
201
+ - nullable booleans for tri-state
202
+
203
+
204
+ naming conventions
205
+
206
+ be consistent:
207
+ - table names: plural, snake_case (users, user_profiles)
208
+ - column names: snake_case (created_at, is_active)
209
+ - primary keys: id or {table}_id
210
+ - foreign keys: {related_table}_id (user_id, organization_id)
211
+ - indexes: idx_{table}_{columns} (idx_users_email)
212
+ - unique constraints: uq_{table}_{columns}
213
+ - foreign key constraints: fk_{table}_{ref_table}
214
+
215
+
216
+ PHASE 3: RELATIONSHIPS AND FOREIGN KEYS
217
+
218
+
219
+ one-to-many relationship
220
+
221
+ <create>
222
+ <file>src/models/post.py</file>
223
+ <content>
224
+ """Post entity with relationships."""
225
+ from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey
226
+ from sqlalchemy.orm import relationship
227
+
228
+ from .user import Base, User
229
+
230
+
231
+ class Post(Base):
232
+ """Blog post authored by a user."""
233
+
234
+ __tablename__ = "posts"
235
+
236
+ id = Column(Integer, primary_key=True, autoincrement=True)
237
+ title = Column(String(200), nullable=False)
238
+ slug = Column(String(200), nullable=False, unique=True, index=True)
239
+ content = Column(Text, nullable=False)
240
+
241
+ # foreign key to users (many-to-one)
242
+ author_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
243
+
244
+ # timestamps
245
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
246
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
247
+ published_at = Column(DateTime)
248
+
249
+ # relationships
250
+ author = relationship("User", back_populates="posts")
251
+
252
+ def __repr__(self):
253
+ return f"<Post(id={self.id}, title={self.title})>"
254
+ </content>
255
+ </create>
256
+
257
+
258
+ add back-reference on User
259
+
260
+ <read><file>src/models/user.py</file></read>
261
+
262
+ <edit>
263
+ <file>src/models/user.py</file>
264
+ <content>
265
+ """User entity model."""
266
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean
267
+ from sqlalchemy.orm import declarative_base, relationship
268
+ from datetime import datetime
269
+
270
+ Base = declarative_base()
271
+
272
+
273
+ class User(Base):
274
+ """User entity representing application users."""
275
+
276
+ __tablename__ = "users"
277
+
278
+ # primary key - always required
279
+ id = Column(Integer, primary_key=True, autoincrement=True)
280
+
281
+ # required attributes
282
+ email = Column(String(255), nullable=False, unique=True, index=True)
283
+ username = Column(String(50), nullable=False, unique=True, index=True)
284
+ password_hash = Column(String(255), nullable=False)
285
+
286
+ # optional attributes
287
+ full_name = Column(String(100))
288
+ avatar_url = Column(String(500))
289
+ bio = Column(String(500))
290
+
291
+ # status fields
292
+ is_active = Column(Boolean, default=True, nullable=False)
293
+ is_verified = Column(Boolean, default=False, nullable=False)
294
+
295
+ # timestamps
296
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
297
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
298
+ last_login_at = Column(DateTime)
299
+
300
+ # relationships
301
+ posts = relationship("Post", back_populates="author")
302
+
303
+ def __repr__(self):
304
+ return f"<User(id={self.id}, username={self.username})>"
305
+ </content>
306
+ </edit>
307
+
308
+
309
+ many-to-many relationship
310
+
311
+ <create>
312
+ <file>src/models/tag.py</file>
313
+ <content>
314
+ """Tag entity and many-to-many relationship with posts."""
315
+ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table
316
+ from sqlalchemy.orm import relationship
317
+
318
+ from .user import Base
319
+
320
+
321
+ # association table for many-to-many relationship
322
+ post_tags = Table(
323
+ "post_tags",
324
+ Base.metadata,
325
+ Column("post_id", Integer, ForeignKey("posts.id"), primary_key=True),
326
+ Column("tag_id", Integer, ForeignKey("tags.id"), primary_key=True),
327
+ Column("assigned_at", DateTime, default=datetime.utcnow)
328
+ )
329
+
330
+
331
+ class Tag(Base):
332
+ """Tag entity for categorizing posts."""
333
+
334
+ __tablename__ = "tags"
335
+
336
+ id = Column(Integer, primary_key=True, autoincrement=True)
337
+ name = Column(String(50), nullable=False, unique=True, index=True)
338
+ slug = Column(String(50), nullable=False, unique=True)
339
+ color = Column(String(7)) # hex color like #ff0000
340
+
341
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
342
+
343
+ # relationships
344
+ posts = relationship("Post", secondary=post_tags, back_populates="tags")
345
+
346
+ def __repr__(self):
347
+ return f"<Tag(id={self.id}, name={self.name})>"
348
+ </content>
349
+ </edit>
350
+
351
+
352
+ add tags relationship to Post
353
+
354
+ <read><file>src/models/post.py</file></read>
355
+
356
+ <edit>
357
+ <file>src/models/post.py</file>
358
+ <find>
359
+ """Post entity with relationships."""
360
+ from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey
361
+ from sqlalchemy.orm import relationship
362
+
363
+ from .user import Base, User
364
+ </find>
365
+ <replace>
366
+ """Post entity with relationships."""
367
+ from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey
368
+ from sqlalchemy.orm import relationship
369
+
370
+ from .user import Base, User
371
+ from .tag import post_tags
372
+ </replace>
373
+ </edit>
374
+
375
+ <edit>
376
+ <file>src/models/post.py</file>
377
+ <find>
378
+ # relationships
379
+ author = relationship("User", back_populates="posts")
380
+ </find>
381
+ <replace>
382
+ # relationships
383
+ author = relationship("User", back_populates="posts")
384
+ tags = relationship("Tag", secondary=post_tags, back_populates="posts")
385
+ </replace>
386
+ </edit>
387
+
388
+
389
+ one-to-one relationship
390
+
391
+ <create>
392
+ <file>src/models/profile.py</file>
393
+ <content>
394
+ """User profile - one-to-one with user."""
395
+ from sqlalchemy import Column, Integer, String, Text, ForeignKey, Date
396
+ from sqlalchemy.orm import relationship
397
+
398
+ from .user import Base
399
+
400
+
401
+ class UserProfile(Base):
402
+ """Extended profile information for users."""
403
+
404
+ __tablename__ = "user_profiles"
405
+
406
+ # one-to-one: user_id is primary key and foreign key
407
+ user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
408
+
409
+ # profile attributes
410
+ phone = Column(String(20))
411
+ address_line1 = Column(String(100))
412
+ address_line2 = Column(String(100))
413
+ city = Column(String(50))
414
+ state = Column(String(50))
415
+ postal_code = Column(String(20))
416
+ country = Column(String(2)) # ISO country code
417
+
418
+ birth_date = Column(Date)
419
+ website = Column(String(200))
420
+ linkedin_url = Column(String(200))
421
+ github_url = Column(String(200))
422
+
423
+ preferences = Column(Text) # JSON string for flexible preferences
424
+
425
+ # relationship
426
+ user = relationship("User", back_populates="profile")
427
+
428
+ def __repr__(self):
429
+ return f"<UserProfile(user_id={self.user_id})>"
430
+ </content>
431
+ </edit>
432
+
433
+
434
+ add profile relationship to User
435
+
436
+ <read><file>src/models/user.py</file></read>
437
+
438
+ <edit>
439
+ <file>src/models/user.py</file>
440
+ <find>
441
+ # relationships
442
+ posts = relationship("Post", back_populates="author")
443
+ </find>
444
+ <replace>
445
+ # relationships
446
+ posts = relationship("Post", back_populates="author")
447
+ profile = relationship("UserProfile", back_populates="user", uselist=False)
448
+ </replace>
449
+ </edit>
450
+
451
+
452
+ PHASE 4: INDEXING STRATEGY
453
+
454
+
455
+ primary indexes
456
+
457
+ every table needs a primary key:
458
+ - most common: auto-increment integer
459
+ - for distributed systems: uuid
460
+ - for natural keys: composite key on unique columns
461
+
462
+
463
+ unique indexes
464
+
465
+ enforce uniqueness and speed lookups:
466
+
467
+ <read><file>src/models/user.py</file></read>
468
+
469
+ <edit>
470
+ <file>src/models/user.py</file>
471
+ <content>
472
+ """User entity model with indexes."""
473
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean, Index, UniqueConstraint
474
+ from sqlalchemy.orm import declarative_base, relationship
475
+ from datetime import datetime
476
+
477
+ Base = declarative_base()
478
+
479
+
480
+ class User(Base):
481
+ """User entity representing application users."""
482
+
483
+ __tablename__ = "users"
484
+
485
+ # primary key - always required
486
+ id = Column(Integer, primary_key=True, autoincrement=True)
487
+
488
+ # required attributes with unique indexes
489
+ email = Column(String(255), nullable=False)
490
+ username = Column(String(50), nullable=False)
491
+ password_hash = Column(String(255), nullable=False)
492
+
493
+ # optional attributes
494
+ full_name = Column(String(100))
495
+ avatar_url = Column(String(500))
496
+ bio = Column(String(500))
497
+
498
+ # status fields
499
+ is_active = Column(Boolean, default=True, nullable=False)
500
+ is_verified = Column(Boolean, default=False, nullable=False)
501
+
502
+ # timestamps
503
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
504
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
505
+ last_login_at = Column(DateTime)
506
+
507
+ # relationships
508
+ posts = relationship("Post", back_populates="author")
509
+ profile = relationship("UserProfile", back_populates="user", uselist=False)
510
+
511
+ # constraints
512
+ __table_args__ = (
513
+ UniqueConstraint("email", name="uq_users_email"),
514
+ UniqueConstraint("username", name="uq_users_username"),
515
+ Index("idx_users_email", "email"),
516
+ Index("idx_users_username", "username"),
517
+ Index("idx_users_is_active", "is_active"),
518
+ Index("idx_users_created_at", "created_at"),
519
+ )
520
+
521
+ def __repr__(self):
522
+ return f"<User(id={self.id}, username={self.username})>"
523
+ </content>
524
+ </edit>
525
+
526
+
527
+ composite indexes
528
+
529
+ for queries filtering on multiple columns:
530
+
531
+ <create>
532
+ <file>src/models/order.py</file>
533
+ <content>
534
+ """Order entity with composite indexes."""
535
+ from sqlalchemy import Column, Integer, String, DateTime, Numeric, ForeignKey, Index
536
+ from sqlalchemy.orm import relationship
537
+
538
+ from .user import Base
539
+
540
+
541
+ class Order(Base):
542
+ """Customer orders."""
543
+
544
+ __tablename__ = "orders"
545
+
546
+ id = Column(Integer, primary_key=True, autoincrement=True)
547
+ order_number = Column(String(50), nullable=False, unique=True)
548
+
549
+ # foreign key
550
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
551
+
552
+ # order details
553
+ status = Column(String(20), nullable=False, default="pending")
554
+ total = Column(Numeric(10, 2), nullable=False)
555
+
556
+ # timestamps
557
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
558
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
559
+
560
+ # relationships
561
+ user = relationship("User")
562
+
563
+ # composite indexes for common query patterns
564
+ __table_args__ = (
565
+ # index for user's orders by status
566
+ Index("idx_orders_user_status", "user_id", "status"),
567
+ # index for user's orders by date
568
+ Index("idx_orders_user_created", "user_id", "created_at"),
569
+ # index for orders by status and date (dashboard query)
570
+ Index("idx_orders_status_created", "status", "created_at"),
571
+ )
572
+ </content>
573
+ </create>
574
+
575
+
576
+ when to index
577
+
578
+ index these columns:
579
+ [x] primary keys (automatic)
580
+ [x] foreign keys
581
+ [x] columns in WHERE clauses
582
+ [x] columns in JOIN conditions
583
+ [x] columns in ORDER BY
584
+ [x] columns frequently searched
585
+
586
+ dont index:
587
+ [x] columns with low cardinality (boolean, small enums)
588
+ [x] columns frequently updated
589
+ [x] very wide columns (long text)
590
+ [x] tables that are write-heavy and rarely read
591
+
592
+
593
+ PHASE 5: MIGRATION SETUP WITH ALEMBIC
594
+
595
+
596
+ initialize alembic
597
+
598
+ <terminal>alembic init alembic</terminal>
599
+
600
+ this creates:
601
+ - alembic/
602
+ - alembic.ini
603
+ - alembic/env.py
604
+
605
+
606
+ configure alembic.ini
607
+
608
+ <read><file>alembic.ini</file></read>
609
+
610
+ <edit>
611
+ <file>alembic.ini</file>
612
+ <find>
613
+ sqlalchemy.url = driver://user:pass@localhost/dbname
614
+ </find>
615
+ <replace>
616
+ sqlalchemy.url = postgresql://user:password@localhost:5432/dbname
617
+ # or use environment variable:
618
+ # sqlalchemy.url = ${DATABASE_URL}
619
+ </replace>
620
+ </edit>
621
+
622
+
623
+ configure env.py
624
+
625
+ <read><file>alembic/env.py</file></read>
626
+
627
+ <edit>
628
+ <file>alembic/env.py</file>
629
+ <find>
630
+ from logging.config import fileConfig
631
+
632
+ from sqlalchemy import engine_from_config
633
+ from sqlalchemy import pool
634
+
635
+ from alembic import context
636
+
637
+ # this is the Alembic Config object
638
+ config = context.config
639
+
640
+ # add your model's MetaData object here
641
+ # for 'autogenerate' support
642
+ # target_metadata = mymodel.Base.metadata
643
+ target_metadata = None
644
+ </find>
645
+ <replace>
646
+ from logging.config import fileConfig
647
+
648
+ from sqlalchemy import engine_from_config
649
+ from sqlalchemy import pool
650
+
651
+ from alembic import context
652
+
653
+ # import your models
654
+ import sys
655
+ from pathlib import Path
656
+ sys.path.append(str(Path(__file__).parent.parent))
657
+
658
+ from src.models.user import Base
659
+ from src.models.post import Post
660
+ from src.models.tag import Tag
661
+ from src.models.profile import UserProfile
662
+ from src.models.order import Order
663
+
664
+ # this is the Alembic Config object
665
+ config = context.config
666
+
667
+ # add your model's MetaData object here
668
+ target_metadata = Base.metadata
669
+ </replace>
670
+ </edit>
671
+
672
+
673
+ PHASE 6: CREATING MIGRATIONS
674
+
675
+
676
+ initial migration
677
+
678
+ <terminal>alembic revision --autogenerate -m "Initial schema"</terminal>
679
+
680
+ this generates a new migration file in alembic/versions/.
681
+
682
+
683
+ review generated migration
684
+
685
+ <read><file>alembic/versions/001_initial_schema.py</file></read>
686
+
687
+ check that:
688
+ [ ] all tables are created
689
+ [ ] indexes are defined
690
+ [ ] foreign keys have proper constraints
691
+ [ ] no extra tables from imports
692
+
693
+
694
+ manual migration for control
695
+
696
+ <terminal>alembic revision -m "Add user email verification"</terminal>
697
+
698
+ <read><file>alembic/versions/002_add_user_email_verification.py</file></read>
699
+
700
+ <edit>
701
+ <file>alembic/versions/002_add_user_email_verification.py</file>
702
+ <find>
703
+ """${message}
704
+
705
+ Revision ID: ${up_revision}
706
+ Revises: ${down_revision}
707
+ Create Date: ${create_date}
708
+
709
+ """
710
+ from alembic import op
711
+ import sqlalchemy as sa
712
+ ${imports if imports else ""}
713
+
714
+ # revision identifiers, used by Alembic.
715
+ revision = ${repr(up_revision)}
716
+ down_revision = ${repr(down_revision)}
717
+ branch_labels = ${repr(branch_labels)}
718
+ depends_on = ${repr(depends_on)}
719
+
720
+ def upgrade() -> None:
721
+ ${upgrades if upgrades else "pass"}
722
+
723
+ def downgrade() -> None:
724
+ ${downgrades if downgrades else "pass"}
725
+ </find>
726
+ <replace>
727
+ """Add user email verification token and timestamp
728
+
729
+ Revision ID: 002_add_verification
730
+ Revises: 001_initial_schema
731
+ Create Date: 2024-01-15
732
+
733
+ """
734
+ from alembic import op
735
+ import sqlalchemy as sa
736
+
737
+ # revision identifiers, used by Alembic.
738
+ revision = "002_add_verification"
739
+ down_revision = "001_initial_schema"
740
+ branch_labels = None
741
+ depends_on = None
742
+
743
+ def upgrade() -> None:
744
+ # add verification_token column
745
+ op.add_column(
746
+ "users",
747
+ sa.Column("verification_token", sa.String(255), nullable=True)
748
+ )
749
+
750
+ # add verified_at column
751
+ op.add_column(
752
+ "users",
753
+ sa.Column("verified_at", sa.DateTime(), nullable=True)
754
+ )
755
+
756
+ # create index for token lookup
757
+ op.create_index(
758
+ "idx_users_verification_token",
759
+ "users",
760
+ ["verification_token"]
761
+ )
762
+
763
+ def downgrade() -> None:
764
+ # remove index
765
+ op.drop_index("idx_users_verification_token", "users")
766
+
767
+ # remove columns
768
+ op.drop_column("users", "verified_at")
769
+ op.drop_column("users", "verification_token")
770
+ </replace>
771
+ </edit>
772
+
773
+
774
+ PHASE 7: RUNNING AND MANAGING MIGRATIONS
775
+
776
+
777
+ apply migrations
778
+
779
+ <terminal>alembic upgrade head</terminal>
780
+
781
+ this runs all pending migrations.
782
+
783
+
784
+ check current version
785
+
786
+ <terminal>alembic current</terminal>
787
+
788
+ view migration history:
789
+ <terminal>alembic history</terminal>
790
+
791
+
792
+ rollback one migration
793
+
794
+ <terminal>alembic downgrade -1</terminal>
795
+
796
+
797
+ rollback to specific version
798
+
799
+ <terminal>alembic downgrade 001_initial_schema</terminal>
800
+
801
+
802
+ redo last migration
803
+
804
+ useful during development when testing migrations:
805
+
806
+ <terminal>alembic downgrade -1 && alembic upgrade head</terminal>
807
+
808
+
809
+ PHASE 8: DATA MIGRATIONS
810
+
811
+
812
+ migrating existing data
813
+
814
+ sometimes you need to transform data during migration:
815
+
816
+ <create>
817
+ <file>alembic/versions/003_migrate_user_status.py</file>
818
+ <content>
819
+ """Migrate user status to separate columns
820
+
821
+ Revision ID: 003_migrate_user_status
822
+ Revises: 002_add_verification
823
+ Create Date: 2024-01-16
824
+
825
+ """
826
+ from alembic import op
827
+ import sqlalchemy as sa
828
+
829
+ revision = "003_migrate_user_status"
830
+ down_revision = "002_add_verification"
831
+ branch_labels = None
832
+ depends_on = None
833
+
834
+ def upgrade() -> None:
835
+ # step 1: add new columns
836
+ op.add_column("users", sa.Column("is_active", sa.Boolean(), nullable=True))
837
+ op.add_column("users", sa.Column("is_banned", sa.Boolean(), nullable=True))
838
+
839
+ # step 2: migrate data from old status column
840
+ connection = op.get_bind()
841
+
842
+ # set is_active based on status
843
+ connection.execute(
844
+ sa.text("""
845
+ UPDATE users
846
+ SET is_active = (status = 'active')
847
+ """)
848
+ )
849
+
850
+ # set is_banned based on status
851
+ connection.execute(
852
+ sa.text("""
853
+ UPDATE users
854
+ SET is_banned = (status = 'banned')
855
+ """)
856
+ )
857
+
858
+ # step 3: make new columns non-nullable with defaults
859
+ op.alter_column("users", "is_active", nullable=False, server_default="true")
860
+ op.alter_column("users", "is_banned", nullable=False, server_default="false")
861
+
862
+ # step 4: remove old status column
863
+ op.drop_column("users", "status")
864
+
865
+ def downgrade() -> None:
866
+ # step 1: add back old status column
867
+ op.add_column("users", sa.Column("status", sa.String(20), nullable=True))
868
+
869
+ # step 2: migrate data back
870
+ connection = op.get_bind()
871
+
872
+ connection.execute(
873
+ sa.text("""
874
+ UPDATE users
875
+ SET status = CASE
876
+ WHEN is_banned THEN 'banned'
877
+ WHEN NOT is_active THEN 'inactive'
878
+ ELSE 'active'
879
+ END
880
+ """)
881
+ )
882
+
883
+ # step 3: make status non-nullable
884
+ op.alter_column("users", "status", nullable=False)
885
+
886
+ # step 4: remove new columns
887
+ op.drop_column("users", "is_banned")
888
+ op.drop_column("users", "is_active")
889
+ </content>
890
+ </create>
891
+
892
+
893
+ PHASE 9: DATABASE CONSTRAINTS
894
+
895
+
896
+ check constraints
897
+
898
+ enforce data rules at database level:
899
+
900
+ <create>
901
+ <file>src/models/constraints.py</file>
902
+ <content>
903
+ """Models with database constraints."""
904
+ from sqlalchemy import Column, Integer, String, DateTime, Numeric, CheckConstraint
905
+ from sqlalchemy.orm import declarative_base
906
+ from datetime import datetime
907
+
908
+ Base = declarative_base()
909
+
910
+
911
+ class Product(Base):
912
+ """Product with validation constraints."""
913
+
914
+ __tablename__ = "products"
915
+
916
+ id = Column(Integer, primary_key=True)
917
+ name = Column(String(100), nullable=False)
918
+ price = Column(Numeric(10, 2), nullable=False)
919
+ quantity = Column(Integer, nullable=False)
920
+ discount_percent = Column(Integer, default=0)
921
+
922
+ created_at = Column(DateTime, default=datetime.utcnow)
923
+
924
+ # constraints
925
+ __table_args__ = (
926
+ # price must be positive
927
+ CheckConstraint("price > 0", name="check_positive_price"),
928
+ # quantity cannot be negative
929
+ CheckConstraint("quantity >= 0", name="check_nonnegative_quantity"),
930
+ # discount between 0 and 100
931
+ CheckConstraint("discount_percent >= 0 AND discount_percent <= 100",
932
+ name="check_valid_discount"),
933
+ # final price after discount must be positive
934
+ CheckConstraint("price * (1 - discount_percent / 100.0) > 0",
935
+ name="check_positive_final_price"),
936
+ )
937
+ </content>
938
+ </create>
939
+
940
+
941
+ enum constraints
942
+
943
+ <create>
944
+ <file>src/models/enums.py</file>
945
+ <content>
946
+ """Models with enum constraints."""
947
+ from sqlalchemy import Column, Integer, String, DateTime, Enum
948
+ from sqlalchemy.orm import declarative_base
949
+ from enum import Enum as PyEnum
950
+ from datetime import datetime
951
+
952
+ Base = declarative_base()
953
+
954
+
955
+ class OrderStatus(PyEnum):
956
+ """Order status enumeration."""
957
+ PENDING = "pending"
958
+ PROCESSING = "processing"
959
+ SHIPPED = "shipped"
960
+ DELIVERED = "delivered"
961
+ CANCELLED = "cancelled"
962
+ REFUNDED = "refunded"
963
+
964
+
965
+ class Order(Base):
966
+ """Order with enum status."""
967
+
968
+ __tablename__ = "orders"
969
+
970
+ id = Column(Integer, primary_key=True)
971
+ status = Column(
972
+ Enum(OrderStatus),
973
+ default=OrderStatus.PENDING,
974
+ nullable=False
975
+ )
976
+
977
+ created_at = Column(DateTime, default=datetime.utcnow)
978
+ </content>
979
+ </create>
980
+
981
+
982
+ PHASE 10: QUERY OPTIMIZATION
983
+
984
+
985
+ avoid N+1 queries
986
+
987
+ the classic anti-pattern:
988
+
989
+ # bad: N+1 query
990
+ users = session.query(User).all()
991
+ for user in users:
992
+ print(user.posts) # each iteration triggers a query!
993
+
994
+ the fix: eager loading
995
+
996
+ from sqlalchemy.orm import selectinload, joinedload
997
+
998
+ # good: single query with join
999
+ users = session.query(User).options(selectinload(User.posts)).all()
1000
+ for user in users:
1001
+ print(user.posts) # already loaded!
1002
+
1003
+
1004
+ select the right join type
1005
+
1006
+ selectinload - good for one-to-many, separate queries
1007
+ users = session.query(User).options(selectinload(User.posts)).all()
1008
+
1009
+ joinedload - good for one-to-one, single query with join
1010
+ users = session.query(User).options(joinedload(User.profile)).all()
1011
+
1012
+ subqueryload - for nested relationships
1013
+ users = session.query(User).options(
1014
+ subqueryload(User.posts).selectinload(Post.tags)
1015
+ ).all()
1016
+
1017
+
1018
+ only select what you need
1019
+
1020
+ # bad: fetches all columns
1021
+ users = session.query(User).all()
1022
+
1023
+ # good: only needed columns
1024
+ user_names = session.query(User.username, User.email).all()
1025
+
1026
+ # good: use entities
1027
+ users = session.query(User.username, User.email).all()
1028
+
1029
+
1030
+ use pagination
1031
+
1032
+ <create>
1033
+ <file>src/repository/pagination.py</file>
1034
+ <content>
1035
+ """Pagination utilities."""
1036
+ from typing import Generic, TypeVar, List
1037
+ from dataclasses import dataclass
1038
+ from sqlalchemy.orm import Query
1039
+
1040
+ T = TypeVar("T")
1041
+
1042
+
1043
+ @dataclass
1044
+ class PaginatedResult(Generic[T]):
1045
+ """Result of paginated query."""
1046
+ items: List[T]
1047
+ total: int
1048
+ page: int
1049
+ page_size: int
1050
+ has_more: bool
1051
+
1052
+ @property
1053
+ def total_pages(self) -> int:
1054
+ """Calculate total pages."""
1055
+ return (self.total + self.page_size - 1) // self.page_size
1056
+
1057
+
1058
+ def paginate(query: Query, page: int = 1, page_size: int = 20) -> PaginatedResult:
1059
+ """Apply pagination to a SQLAlchemy query."""
1060
+ # count total
1061
+ total = query.count()
1062
+
1063
+ # calculate offset
1064
+ offset = (page - 1) * page_size
1065
+
1066
+ # fetch page
1067
+ items = query.offset(offset).limit(page_size).all()
1068
+
1069
+ return PaginatedResult(
1070
+ items=items,
1071
+ total=total,
1072
+ page=page,
1073
+ page_size=page_size,
1074
+ has_more=offset + page_size < total
1075
+ )
1076
+ </content>
1077
+ </create>
1078
+
1079
+
1080
+ PHASE 11: TRANSACTION MANAGEMENT
1081
+
1082
+
1083
+ transaction basics
1084
+
1085
+ from sqlalchemy import create_engine
1086
+ from sqlalchemy.orm import sessionmaker
1087
+
1088
+ engine = create_engine("postgresql://...")
1089
+ Session = sessionmaker(bind=engine)
1090
+
1091
+
1092
+ # explicit transaction
1093
+ session = Session()
1094
+ try:
1095
+ user = User(username="alice", email="alice@example.com")
1096
+ session.add(user)
1097
+ session.commit()
1098
+ except Exception as e:
1099
+ session.rollback()
1100
+ raise
1101
+ finally:
1102
+ session.close()
1103
+
1104
+
1105
+ context manager for transactions
1106
+
1107
+ <create>
1108
+ <file>src/database/transaction.py</file>
1109
+ <content>
1110
+ """Transaction management utilities."""
1111
+ from contextlib import contextmanager
1112
+ from typing import Generator
1113
+ from sqlalchemy.orm import Session
1114
+
1115
+
1116
+ @contextmanager
1117
+ def transaction(session: Session) -> Generator[Session, None, None]:
1118
+ """Context manager for database transactions.
1119
+
1120
+ Usage:
1121
+ with transaction(session) as s:
1122
+ user = User(username="alice")
1123
+ s.add(user)
1124
+ # automatically commits on success, rolls back on error
1125
+ """
1126
+ try:
1127
+ yield session
1128
+ session.commit()
1129
+ except Exception:
1130
+ session.rollback()
1131
+ raise
1132
+
1133
+
1134
+ @contextmanager
1135
+ def nested_transaction(session: Session) -> Generator[Session, None, None]:
1136
+ """Context manager for nested (savepoint) transactions.
1137
+
1138
+ Useful for tests or when you need to rollback inner transactions
1139
+ while keeping outer ones.
1140
+ """
1141
+ try:
1142
+ nested = session.begin_nested()
1143
+ yield session
1144
+ nested.commit()
1145
+ except Exception:
1146
+ nested.rollback()
1147
+ raise
1148
+ </content>
1149
+ </create>
1150
+
1151
+
1152
+ PHASE 12: SOFT DELETES
1153
+
1154
+
1155
+ implementing soft deletes
1156
+
1157
+ <create>
1158
+ <file>src/models/soft_delete.py</file>
1159
+ <content>
1160
+ """Soft delete mixin for models."""
1161
+ from sqlalchemy import Column, DateTime, Boolean, event
1162
+ from sqlalchemy.orm import declarative_mixin
1163
+ from datetime import datetime
1164
+
1165
+
1166
+ @declarative_mixin
1167
+ class SoftDeleteMixin:
1168
+ """Add soft delete functionality to a model."""
1169
+
1170
+ deleted_at = Column(DateTime, nullable=True, index=True)
1171
+ is_deleted = Column(Boolean, default=False, nullable=False, index=True)
1172
+
1173
+ def soft_delete(self):
1174
+ """Mark record as deleted without removing from database."""
1175
+ self.is_deleted = True
1176
+ self.deleted_at = datetime.utcnow()
1177
+
1178
+ def restore(self):
1179
+ """Restore soft-deleted record."""
1180
+ self.is_deleted = False
1181
+ self.deleted_at = None
1182
+
1183
+
1184
+ class SoftDeleteQuery:
1185
+ """Query mixin for filtering soft-deleted records."""
1186
+
1187
+ def _with_deleted(self):
1188
+ """Include soft-deleted records."""
1189
+ return self.enable_assertions(False).filter()
1190
+
1191
+ def without_deleted(self):
1192
+ """Exclude soft-deleted records."""
1193
+ return self.filter_by(is_deleted=False)
1194
+ </content>
1195
+ </create>
1196
+
1197
+
1198
+ using soft deletes in models
1199
+
1200
+ from sqlalchemy import Column, Integer, String
1201
+ from sqlalchemy.orm import declarative_base
1202
+
1203
+ Base = declarative_base()
1204
+
1205
+
1206
+ class User(SoftDeleteMixin, Base):
1207
+ """User with soft delete."""
1208
+ __tablename__ = "users"
1209
+
1210
+ id = Column(Integer, primary_key=True)
1211
+ username = Column(String(50), nullable=False)
1212
+ email = Column(String(255), nullable=False)
1213
+
1214
+
1215
+ PHASE 13: AUDIT COLUMNS AND HISTORY
1216
+
1217
+
1218
+ audit columns
1219
+
1220
+ <create>
1221
+ <file>src/models/audit.py</file>
1222
+ <content>
1223
+ """Audit columns for tracking record changes."""
1224
+ from sqlalchemy import Column, DateTime, Integer, ForeignKey, event
1225
+ from sqlalchemy.orm import declarative_mixin, relationship
1226
+ from datetime import datetime
1227
+
1228
+
1229
+ @declarative_mixin
1230
+ class AuditMixin:
1231
+ """Add audit columns to track record lifecycle."""
1232
+
1233
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
1234
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
1235
+ created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
1236
+ updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
1237
+
1238
+ # relationships
1239
+ created_by = relationship("User", foreign_keys=[created_by_id])
1240
+ updated_by = relationship("User", foreign_keys=[updated_by_id])
1241
+
1242
+
1243
+ @declarative_mixin
1244
+ class FullAuditMixin(AuditMixin):
1245
+ """Extended audit with deletion tracking."""
1246
+
1247
+ deleted_at = Column(DateTime, nullable=True)
1248
+ deleted_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
1249
+
1250
+ # relationship
1251
+ deleted_by = relationship("User", foreign_keys=[deleted_by_id])
1252
+ </content>
1253
+ </create>
1254
+
1255
+
1256
+ audit table for history tracking
1257
+
1258
+ <create>
1259
+ <file>src/models/audit_log.py</file>
1260
+ <content>
1261
+ """Audit log table for tracking changes."""
1262
+ from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, JSON
1263
+ from sqlalchemy.orm import declarative_base, relationship
1264
+ from datetime import datetime
1265
+
1266
+ Base = declarative_base()
1267
+
1268
+
1269
+ class AuditLog(Base):
1270
+ """Record of all changes to tracked entities."""
1271
+
1272
+ __tablename__ = "audit_logs"
1273
+
1274
+ id = Column(Integer, primary_key=True, autoincrement=True)
1275
+
1276
+ # what changed
1277
+ table_name = Column(String(100), nullable=False, index=True)
1278
+ record_id = Column(Integer, nullable=False, index=True)
1279
+ action = Column(String(20), nullable=False) # INSERT, UPDATE, DELETE
1280
+
1281
+ # who changed it
1282
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
1283
+ user = relationship("User", foreign_keys=[user_id])
1284
+
1285
+ # what changed
1286
+ old_values = Column(JSON) # {"field": "old_value"}
1287
+ new_values = Column(JSON) # {"field": "new_value"}
1288
+ changed_fields = Column(JSON) # ["field1", "field2"]
1289
+
1290
+ # metadata
1291
+ ip_address = Column(String(45)) # IPv6 support
1292
+ user_agent = Column(String(500))
1293
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
1294
+
1295
+ def __repr__(self):
1296
+ return f"<AuditLog(table={self.table_name}, record_id={self.record_id}, action={self.action})>"
1297
+ </content>
1298
+ </create>
1299
+
1300
+
1301
+ PHASE 14: DATABASE TESTING
1302
+
1303
+
1304
+ test database fixtures
1305
+
1306
+ <create>
1307
+ <file>tests/conftest.py</file>
1308
+ <content>
1309
+ """Pytest fixtures for database testing."""
1310
+ import pytest
1311
+ from sqlalchemy import create_engine
1312
+ from sqlalchemy.orm import sessionmaker
1313
+ from tempfile import NamedTemporaryFile
1314
+ import os
1315
+
1316
+ from src.models.user import Base
1317
+ from src.models.post import Post
1318
+ from src.models.tag import Tag
1319
+
1320
+
1321
+ @pytest.fixture(scope="function")
1322
+ def test_db():
1323
+ """Create an in-memory SQLite database for testing."""
1324
+ engine = create_engine("sqlite:///:memory:")
1325
+
1326
+ # create all tables
1327
+ Base.metadata.create_all(engine)
1328
+
1329
+ yield engine
1330
+
1331
+ # cleanup
1332
+ Base.metadata.drop_all(engine)
1333
+
1334
+
1335
+ @pytest.fixture(scope="function")
1336
+ def db_session(test_db):
1337
+ """Create a database session for testing."""
1338
+ Session = sessionmaker(bind=test_db)
1339
+ session = Session()
1340
+
1341
+ yield session
1342
+
1343
+ session.close()
1344
+
1345
+
1346
+ @pytest.fixture
1347
+ def sample_user(db_session):
1348
+ """Create a sample user for tests."""
1349
+ user = User(
1350
+ username="testuser",
1351
+ email="test@example.com",
1352
+ password_hash="hashed_password"
1353
+ )
1354
+ db_session.add(user)
1355
+ db_session.commit()
1356
+ return user
1357
+ </content>
1358
+ </create>
1359
+
1360
+
1361
+ example tests
1362
+
1363
+ <create>
1364
+ <file>tests/test_models.py</file>
1365
+ <content>
1366
+ """Tests for database models."""
1367
+ import pytest
1368
+ from sqlalchemy.exc import IntegrityError
1369
+
1370
+ from src.models.user import User
1371
+ from src.models.post import Post
1372
+
1373
+
1374
+ def test_create_user_succeeds(db_session):
1375
+ """Test creating a user."""
1376
+ user = User(
1377
+ username="alice",
1378
+ email="alice@example.com",
1379
+ password_hash="hashed"
1380
+ )
1381
+ db_session.add(user)
1382
+ db_session.commit()
1383
+
1384
+ assert user.id is not None
1385
+ assert user.username == "alice"
1386
+ assert user.is_active is True
1387
+
1388
+
1389
+ def test_unique_email_enforced(db_session):
1390
+ """Test that duplicate emails are rejected."""
1391
+ user1 = User(username="user1", email="same@example.com", password_hash="hash1")
1392
+ user2 = User(username="user2", email="same@example.com", password_hash="hash2")
1393
+
1394
+ db_session.add(user1)
1395
+ db_session.commit()
1396
+
1397
+ db_session.add(user2)
1398
+ with pytest.raises(IntegrityError):
1399
+ db_session.commit()
1400
+
1401
+
1402
+ def test_user_post_relationship(db_session):
1403
+ """Test that users can have posts."""
1404
+ user = User(username="alice", email="alice@example.com", password_hash="hashed")
1405
+ post = Post(title="Test", slug="test", content="Content", author_id=1)
1406
+
1407
+ db_session.add(user)
1408
+ db_session.commit()
1409
+
1410
+ post.author_id = user.id
1411
+ db_session.add(post)
1412
+ db_session.commit()
1413
+
1414
+ assert post.author_id == user.id
1415
+ assert len(user.posts) == 1
1416
+ assert user.posts[0].title == "Test"
1417
+
1418
+
1419
+ def test_soft_delete(db_session):
1420
+ """Test soft delete functionality."""
1421
+ user = User(username="alice", email="alice@example.com", password_hash="hashed")
1422
+ db_session.add(user)
1423
+ db_session.commit()
1424
+
1425
+ user_id = user.id
1426
+ user.soft_delete()
1427
+ db_session.commit()
1428
+
1429
+ # verify soft deleted
1430
+ retrieved = db_session.query(User).filter_by(id=user_id).first()
1431
+ assert retrieved.is_deleted is True
1432
+ assert retrieved.deleted_at is not None
1433
+ </content>
1434
+ </create>
1435
+
1436
+
1437
+ PHASE 15: DATABASE RULES (STRICT MODE)
1438
+
1439
+
1440
+ while this skill is active, these rules are MANDATORY:
1441
+
1442
+ [1] ALWAYS use migrations for schema changes
1443
+ never modify database schema directly
1444
+ every change must be reproducible
1445
+
1446
+ [2] NEVER use reserved words as identifiers
1447
+ avoid: user, order, group, select, where
1448
+ use: users, orders, groups, or prefix: app_user
1449
+
1450
+ [3] EVERY table needs a primary key
1451
+ no exceptions
1452
+ prefer auto-increment integers for new tables
1453
+
1454
+ [4] FOREIGN KEYS are not optional
1455
+ enforce referential integrity at database level
1456
+ add proper indexes on foreign keys
1457
+
1458
+ [5] TIMESTAMPS on every table
1459
+ created_at is mandatory
1460
+ updated_at for mutable data
1461
+ deleted_at for soft deletes
1462
+
1463
+ [6] WRITE both upgrade and downgrade
1464
+ migrations must be reversible
1465
+ never leave a migration half-written
1466
+
1467
+ [7] TEST migrations on sample data
1468
+ verify upgrade works
1469
+ verify downgrade works
1470
+ verify data migration is correct
1471
+
1472
+ [8] USE appropriate data types
1473
+ money: Numeric, not Float
1474
+ enums: CHECK constraint or ENUM type
1475
+ json: JSON/JSONB type, not Text
1476
+
1477
+ [9] INDEX before you need it
1478
+ add indexes for foreign keys
1479
+ add indexes for query patterns
1480
+ review index usage periodically
1481
+
1482
+ [10] DOCUMENT your schema
1483
+ explain non-obvious relationships
1484
+ document denormalization decisions
1485
+ keep ER diagrams up to date
1486
+
1487
+
1488
+ FINAL REMINDERS
1489
+
1490
+
1491
+ database design is foundational
1492
+
1493
+ get it wrong and everything suffers.
1494
+ migrations are painful. refactoring is harder.
1495
+ think carefully before creating tables.
1496
+
1497
+
1498
+ the database is source of truth
1499
+
1500
+ code can change. database persists.
1501
+ ensure the schema reflects reality.
1502
+ constraints and validations belong in the database.
1503
+
1504
+
1505
+ normalize first, optimize later
1506
+
1507
+ start with normalized design.
1508
+ measure performance.
1509
+ denormalize only when needed.
1510
+
1511
+
1512
+ migrations are code
1513
+
1514
+ treat them with same care as application code.
1515
+ review them. test them. version them.
1516
+
1517
+
1518
+ when in doubt
1519
+
1520
+ add a timestamp.
1521
+ add an index.
1522
+ write a migration.
1523
+ measure twice, alter once.
1524
+
1525
+ now go design some schemas.