fix: preserve extensionless document suffix on GaussDB (#18483)

### Summary

RAGFlow's "Create empty document" flow accepts names without a file
extension. The `POST /datasets/<dataset_id>/documents?type=empty` route
calls `_upload_empty_document()`, where `Path(name).suffix.lstrip(".")`
returns `""`.

In GaussDB's A/ORA compatibility mode, that empty string is persisted as
SQL `NULL`. Because `document.suffix` was defined as `NOT NULL`, the
insert failed with a constraint violation.
This commit is contained in:
Sevenzuo
2026-08-19 18:26:14 +08:00
committed by GitHub
parent ece9638f94
commit dd1f335ba2
3 changed files with 25 additions and 11 deletions

View File

@@ -1321,7 +1321,7 @@ class Document(DataBaseModel):
progress_msg = TextField(null=True, help_text="process message", default="")
process_begin_at = DateTimeField(null=True, index=True)
process_duration = FloatField(default=0)
suffix = CharField(max_length=32, null=False, help_text="The real file extension suffix", index=True)
suffix = EmptyStringCharField(max_length=32, null=False, help_text="The real file extension suffix", index=True)
content_hash = CharField(max_length=32, null=True, help_text="xxhash128 of document content for change detection", default="", index=True)
@@ -1895,6 +1895,7 @@ GAUSSDB_EMPTY_STRING_COMPATIBLE_COLUMNS = (
("dialog", ("llm_id", "rerank_id")),
("memory", ("embd_id", "llm_id")),
("file", ("source_type",)),
("document", ("suffix",)),
("system_settings", ("value",)),
("task", ("task_type",)),
("sync_logs", ("error_msg", "full_exception_trace")),
@@ -2395,7 +2396,7 @@ def migrate_db():
alter_db_add_column(migrator, "mcp_server", "variables", JSONField(null=True, help_text="MCP Server variables", default=dict))
alter_db_rename_column(migrator, "task", "process_duation", "process_duration")
alter_db_rename_column(migrator, "document", "process_duation", "process_duration")
alter_db_add_column(migrator, "document", "suffix", CharField(max_length=32, null=False, default="", help_text="The real file extension suffix", index=True))
alter_db_add_column(migrator, "document", "suffix", EmptyStringCharField(max_length=32, null=False, default="", help_text="The real file extension suffix", index=True))
alter_db_add_column(migrator, "api_4_conversation", "errors", TextField(null=True, help_text="errors"))
alter_db_add_column(migrator, "dialog", "meta_data_filter", JSONField(null=True, default={}))
alter_db_column_type(migrator, "canvas_template", "title", JSONField(null=True, default=dict, help_text="Canvas title"))

View File

@@ -75,6 +75,7 @@ def test_gaussdb_empty_string_compatible_migration_drops_not_null_only_for_gauss
assert len(FakeDB.queries) == expected_query_count
assert ('ALTER TABLE "user" ALTER COLUMN "nickname" DROP NOT NULL', None) in FakeDB.queries
assert ('ALTER TABLE "tenant" ALTER COLUMN "llm_id" DROP NOT NULL', None) in FakeDB.queries
assert ('ALTER TABLE "document" ALTER COLUMN "suffix" DROP NOT NULL', None) in FakeDB.queries
assert ('ALTER TABLE "system_settings" ALTER COLUMN "value" DROP NOT NULL', None) in FakeDB.queries
assert ('ALTER TABLE "task" ALTER COLUMN "task_type" DROP NOT NULL', None) in FakeDB.queries
assert ('ALTER TABLE "sync_logs" ALTER COLUMN "error_msg" DROP NOT NULL', None) in FakeDB.queries
@@ -88,15 +89,14 @@ def test_gaussdb_empty_string_compatible_migration_drops_not_null_only_for_gauss
assert len(FakeDB.queries) == expected_query_count
def test_gaussdb_migration_adds_compatible_tags_before_relaxing_columns(monkeypatch):
def test_gaussdb_migration_adds_compatible_fields_before_relaxing_columns(monkeypatch):
events = []
migrated_tags_field = None
migrated_fields = {}
def record_add_column(_migrator, table_name, column_name, column_type):
nonlocal migrated_tags_field
events.append(("add", table_name, column_name))
if (table_name, column_name) == ("user_canvas", "tags"):
migrated_tags_field = column_type
if (table_name, column_name) in {("document", "suffix"), ("user_canvas", "tags")}:
migrated_fields[(table_name, column_name)] = column_type
monkeypatch.setattr(settings, "DATABASE_TYPE", "gaussdb")
monkeypatch.setattr(db_models, "alter_db_add_column", record_add_column)
@@ -111,11 +111,22 @@ def test_gaussdb_migration_adds_compatible_tags_before_relaxing_columns(monkeypa
db_models.migrate_db()
assert isinstance(migrated_tags_field, db_models.EmptyStringCharField)
assert migrated_tags_field.null is True
for field in migrated_fields.values():
assert isinstance(field, db_models.EmptyStringCharField)
assert field.null is True
assert set(migrated_fields) == {("document", "suffix"), ("user_canvas", "tags")}
assert events[-1] == ("relax",)
def test_empty_string_char_field_keeps_non_gaussdb_constraints(monkeypatch):
for database_type in ("mysql", "postgres", "oceanbase"):
monkeypatch.setattr(settings, "DATABASE_TYPE", database_type)
field = db_models.EmptyStringCharField(null=False)
assert field.null is False
assert field.db_value("") == ""
def test_gaussdb_unique_email_migration_checks_unique_email_index_not_fixed_name(monkeypatch):
class Cursor:
def fetchone(self):

View File

@@ -77,7 +77,7 @@ print("gaussdb-adapter-ok")
def test_gaussdb_empty_string_compatible_fields_are_nullable_only_for_gaussdb():
gaussdb_result = run_isolated_flow(
"""
from api.db.db_models import API4Conversation, Dialog, File, Knowledgebase, Memory, SyncLogs, SystemSettings, Task, Tenant, User, UserCanvas
from api.db.db_models import API4Conversation, Dialog, Document, File, Knowledgebase, Memory, SyncLogs, SystemSettings, Task, Tenant, User, UserCanvas
fields = [
User.nickname,
@@ -92,6 +92,7 @@ fields = [
Dialog.rerank_id,
Memory.embd_id,
Memory.llm_id,
Document.suffix,
SystemSettings.value,
Task.task_type,
Task.progress_msg,
@@ -135,7 +136,7 @@ print("gaussdb-empty-string-compatible-ok")
mysql_result = run_isolated_flow(
"""
from api.db.db_models import API4Conversation, Dialog, File, Knowledgebase, Memory, SyncLogs, SystemSettings, Task, Tenant, User, UserCanvas
from api.db.db_models import API4Conversation, Dialog, Document, File, Knowledgebase, Memory, SyncLogs, SystemSettings, Task, Tenant, User, UserCanvas
fields = [
(User.nickname, False),
@@ -150,6 +151,7 @@ fields = [
(Dialog.rerank_id, False),
(Memory.embd_id, False),
(Memory.llm_id, False),
(Document.suffix, False),
(SystemSettings.value, False),
(Task.task_type, False),
(Task.progress_msg, True),