How RAGFlow Assigns create_time in BaseModel
How RAGFlow Assigns create_time in BaseModel
RAGFlow maintains dual implementations in Python and Go for its backend services. One common pattern across both is how timestamps — particularly create_time — are automatically assigned when records are created. This post traces the assignment mechanism through the codebase, using the Dialog entity as a concrete example.
Overview
Every persistent entity in RAGFlow inherits from a BaseModel that carries four standard time-tracking fields:
| Field | Type | Description |
|---|---|---|
create_time |
integer (ms) | Unix timestamp when the record was created |
create_date |
datetime | Human-readable creation datetime |
update_time |
integer (ms) | Unix timestamp of the last update |
update_date |
datetime | Human-readable last-update datetime |
These fields are never set by callers explicitly; instead, the framework handles them at the model or service layer.
Python Implementation
Python uses a layered approach where timestamps are injected at two levels: the ORM model and the service layer.
1. BaseModel.insert()
At the lowest level, BaseModel.insert() in api/db/db_models.py overrides Peewee’s default insert to inject create_time automatically:
@classmethod
def insert(cls, __data=None, **insert):
if isinstance(__data, dict) and __data:
__data[cls._meta.combined["create_time"]] = current_timestamp()
if insert:
insert["create_time"] = current_timestamp()
return super().insert(__data, **insert)
This guarantees that any direct model-level insert always carries a creation timestamp, regardless of what the caller provides.
2. CommonService.insert()
One layer above, CommonService.insert() in api/db/services/common_service.py provides a richer insertion flow. It generates a UUID, sets all four time fields, and delegates to Peewee’s save(force_insert=True):
@classmethod
@DB.connection_context()
def insert(cls, **kwargs):
if "id" not in kwargs:
kwargs["id"] = get_uuid()
timestamp = current_timestamp()
cur_datetime = datetime_format(datetime.now())
kwargs["create_time"] = timestamp
kwargs["create_date"] = cur_datetime
kwargs["update_time"] = timestamp
kwargs["update_date"] = cur_datetime
sample_obj = cls.model(**kwargs).save(force_insert=True)
return sample_obj
This is the primary entry point for creating records in application code.
3. DialogService.save()
DialogService inherits from CommonService. Its save() method in api/db/services/dialog_service.py simply calls save(force_insert=True) on the model, relying on the inherited insert() and _normalize_data() hooks to handle timestamps:
def save(cls, **kwargs):
sample_obj = cls.model(**kwargs).save(force_insert=True)
return sample_obj
4. Automatic update_time via _normalize_data()
Whenever a record is inserted or updated, Peewee calls _normalize_data(). RAGFlow’s override in BaseModel uses this hook to always refresh update_time and to derive *_date fields from their corresponding *_time timestamps:
@classmethod
def _normalize_data(cls, data, kwargs):
normalized = super()._normalize_data(data, kwargs)
if not normalized:
return {}
normalized[cls._meta.combined["update_time"]] = current_timestamp()
for f_n in AUTO_DATE_TIMESTAMP_FIELD_PREFIX:
if {f"{f_n}_time", f"{f_n}_date"}.issubset(cls._meta.combined.keys()) \
and cls._meta.combined[f"{f_n}_time"] in normalized \
and normalized[cls._meta.combined[f"{f_n}_time"]] is not None:
normalized[cls._meta.combined[f"{f_n}_date"]] = \
timestamp_to_date(normalized[cls._meta.combined[f"{f_n}_time"]])
return normalized
This means update_time is refreshed on every write, and human-readable date fields stay in sync with their timestamp counterparts automatically.
Go Implementation
The Go backend takes a more explicit, imperative approach — there are no ORM hooks, so every service function must set the time fields manually.
1. ChatService.SetDialog()
When creating a new dialog (chat) in internal/service/chat.go, the service function captures the current time and assigns all four fields directly on the struct:
now := time.Now().Truncate(time.Second)
createTime := now.UnixMilli()
chat := &entity.Chat{
ID: newID,
TenantID: tenantID,
Name: &name,
// ... other fields ...
}
chat.CreateTime = &createTime
chat.CreateDate = &now
chat.UpdateTime = &createTime
chat.UpdateDate = &now
Note how time.Now() is truncated to second precision for the date fields, while the millisecond Unix timestamp is used for the time fields.
2. ChatSessionService.SetChatSession()
The same pattern appears in internal/service/chat_session.go when creating a chat session:
now := time.Now().Truncate(time.Second)
createTime := time.Now().UnixMilli()
session := &entity.ChatSession{
ID: newID,
DialogID: req.DialogID,
Name: &name,
// ... other fields ...
}
session.CreateTime = &createTime
session.CreateDate = &now
session.UpdateTime = &createTime
session.UpdateDate = &now
3. Admin Service — Tenant Creation
In internal/admin/service.go, the BaseModel struct is embedded directly in the entity literal when creating a tenant:
tenant := &entity.Tenant{
ID: userID,
Name: &tenantName,
// ... other fields ...
BaseModel: entity.BaseModel{
CreateTime: &now,
CreateDate: &nowDate,
UpdateTime: &now,
UpdateDate: &nowDate,
},
}
This is a slightly different style — embedding BaseModel inline — but the effect is the same.
Python vs. Go: Key Differences
| Aspect | Python | Go |
|---|---|---|
| Timestamp injection | Automatic via ORM hooks (insert(), _normalize_data()) |
Manual assignment in each service function |
update_time on updates |
Automatically refreshed by _normalize_data() |
Must be set explicitly by the caller |
| Date-from-timestamp sync | Handled by _normalize_data() loop |
Developer responsibility |
| Risk of missing fields | Low — hooks act as safety nets | Higher — easy to forget in new code |
Takeaways
- Python’s hook-based approach minimizes boilerplate and reduces the chance of missing a timestamp field, but adds implicit behavior that can be hard to trace for newcomers.
- Go’s explicit approach is more verbose but also more transparent — every field assignment is visible at the call site.
- Both implementations store timestamps as millisecond-precision Unix integers alongside a human-readable datetime, a practical pattern for systems that need both machine-efficient sorting and user-facing display.
When contributing to RAGFlow, the key rule is: never set create_time or update_time manually in Python (the framework handles it), but always set them explicitly in Go (nothing will do it for you).