@@ -163,6 +163,89 @@ Mouse().insert1({"mouse_id": 1})
163163Mouse().fetch()
164164```
165165
166+ ## Architecture
167+
168+ ### Object graph
169+
170+ There is exactly ** one** global ` Config ` object created at import time in ` settings.py ` . Both the legacy API and the ` Instance ` API hang off ` Connection ` objects, each of which carries a ` _config ` reference.
171+
172+ ```
173+ settings.py
174+ config = _create_config() ← THE single global Config
175+
176+ instance.py
177+ _global_config = settings.config ← same object (not a copy)
178+ _singleton_connection = None ← lazily created Connection
179+
180+ __init__.py
181+ dj.config = _ConfigProxy() ← proxy → _global_config (with thread-safety check)
182+ dj.conn() ← returns _singleton_connection
183+ dj.Schema() ← uses _singleton_connection
184+ dj.FreeTable() ← uses _singleton_connection
185+
186+ Connection (singleton)
187+ _config → _global_config ← same Config that dj.config writes to
188+
189+ Connection (Instance)
190+ _config → fresh Config ← isolated per-instance
191+ ```
192+
193+ ### Config flow: singleton path
194+
195+ ```
196+ dj.config["safemode"] = False
197+ ↓ _ConfigProxy.__setitem__
198+ _global_config["safemode"] = False (same object as settings.config)
199+ ↓
200+ Connection._config["safemode"] (points to _global_config)
201+ ↓
202+ schema.drop() reads self.connection._config["safemode"] → False ✓
203+ ```
204+
205+ ### Config flow: Instance path
206+
207+ ```
208+ inst = dj.Instance(host=..., user=..., password=...)
209+ ↓
210+ inst.config = _create_config() (fresh Config, independent)
211+ inst.connection._config = inst.config
212+ ↓
213+ inst.config["safemode"] = False
214+ ↓
215+ schema.drop() reads self.connection._config["safemode"] → False ✓
216+ ```
217+
218+ ### Key invariant
219+
220+ ** All runtime config reads go through ` self.connection._config ` ** , never through the global ` config ` directly. This ensures both the singleton and Instance paths read the correct config.
221+
222+ ### Connection-scoped config reads
223+
224+ Every module that previously imported ` from .settings import config ` now reads config from the connection:
225+
226+ | Module | What was read | How it's read now |
227+ | --------| --------------| -------------------|
228+ | ` schemas.py ` | ` config["safemode"] ` , ` config.database.create_tables ` | ` self.connection._config[...] ` |
229+ | ` table.py ` | ` config["safemode"] ` in ` delete() ` , ` drop() ` | ` self.connection._config["safemode"] ` |
230+ | ` expression.py ` | ` config["loglevel"] ` in ` __repr__() ` | ` self.connection._config["loglevel"] ` |
231+ | ` preview.py ` | ` config["display.*"] ` (8 reads) | ` query_expression.connection._config[...] ` |
232+ | ` autopopulate.py ` | ` config.jobs.allow_new_pk_fields ` , ` auto_refresh ` | ` self.connection._config.jobs.* ` |
233+ | ` jobs.py ` | ` config.jobs.default_priority ` , ` stale_timeout ` , ` keep_completed ` | ` self.connection._config.jobs.* ` |
234+ | ` declare.py ` | ` config.jobs.add_job_metadata ` | ` config ` param (threaded from ` table.py ` ) |
235+ | ` diagram.py ` | ` config.display.diagram_direction ` | ` self._connection._config.display.* ` |
236+ | ` staged_insert.py ` | ` config.get_store_spec() ` | ` self._table.connection._config.get_store_spec() ` |
237+
238+ ### Functions that receive config as a parameter
239+
240+ Some module-level functions cannot access ` self.connection ` . Config is threaded through:
241+
242+ | Function | Caller | How config arrives |
243+ | ----------| --------| --------------------|
244+ | ` declare() ` in ` declare.py ` | ` Table.declare() ` in ` table.py ` | ` config=self.connection._config ` kwarg |
245+ | ` _get_job_version() ` in ` jobs.py ` | ` AutoPopulate._make_tuples() ` , ` Job.reserve() ` | ` config=self.connection._config ` positional arg |
246+
247+ Both functions accept ` config=None ` and fall back to the global ` settings.config ` for backward compatibility.
248+
166249## Implementation
167250
168251### 1. Create Instance class
@@ -185,8 +268,11 @@ class Instance:
185268### 2. Global config and singleton connection
186269
187270``` python
188- # Module level
189- _global_config = _create_config() # Created at import time
271+ # settings.py - THE single global config
272+ config = _create_config() # Created at import time
273+
274+ # instance.py - reuses the same config object
275+ _global_config = settings.config # Same reference, not a copy
190276_singleton_connection = None # Created lazily
191277
192278def _check_thread_safe ():
@@ -224,8 +310,12 @@ class _ConfigProxy:
224310
225311config = _ConfigProxy()
226312
227- # dj.conn() -> singleton connection
228- def conn ():
313+ # dj.conn() -> singleton connection (persistent across calls)
314+ def conn (host = None , user = None , password = None , * , reset = False ):
315+ _check_thread_safe()
316+ if reset or (_singleton_connection is None and credentials_provided):
317+ _singleton_connection = Connection(... )
318+ _singleton_connection._config = _global_config
229319 return _get_singleton_connection()
230320
231321# dj.Schema() -> uses singleton connection
@@ -238,21 +328,12 @@ def Schema(name, connection=None, **kwargs):
238328# dj.FreeTable() -> uses singleton connection
239329def FreeTable (conn_or_name , full_table_name = None ):
240330 if full_table_name is None :
241- # Called as FreeTable("db.table")
242331 _check_thread_safe()
243332 return _FreeTable(_get_singleton_connection(), conn_or_name)
244333 else :
245- # Called as FreeTable(conn, "db.table")
246334 return _FreeTable(conn_or_name, full_table_name)
247335```
248336
249- ### 4. Refactor internal code
250-
251- All internal code uses ` self.connection._config ` instead of global ` config ` :
252- - Connection stores reference to its config as ` self._config `
253- - Tables access config via ` self.connection._config `
254- - This works uniformly for both singleton and isolated instances
255-
256337## Global State Audit
257338
258339All module-level mutable state was reviewed for thread-safety implications.
0 commit comments