mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-13 16:38:26 +08:00
Feat: enable sync deleted files for RDBMS & fix remove last file issue (#14615)
### What problem does this PR solve? Feat: enable sync deleted files for RDBMS & fix remove last file issue ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@@ -95,6 +95,18 @@ class _FakeSync(sync_data_source.SyncBase):
|
||||
return self._generate_output
|
||||
|
||||
|
||||
def _make_fake_doc(doc_id="doc-1", updated_at=None):
|
||||
return types.SimpleNamespace(
|
||||
id=doc_id,
|
||||
semantic_identifier=doc_id,
|
||||
extension=".txt",
|
||||
size_bytes=1,
|
||||
doc_updated_at=updated_at or datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
blob=b"x",
|
||||
metadata=None,
|
||||
)
|
||||
|
||||
|
||||
def _make_task():
|
||||
return {
|
||||
"id": "task-1",
|
||||
@@ -121,19 +133,35 @@ def _patch_common_dependencies(monkeypatch):
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.p2
|
||||
async def test_run_task_logic_skips_cleanup_for_empty_snapshot(monkeypatch):
|
||||
async def test_run_task_logic_cleans_up_for_empty_snapshot(monkeypatch):
|
||||
cleanup_calls = []
|
||||
|
||||
_patch_common_dependencies(monkeypatch)
|
||||
|
||||
def _fake_cleanup(*args, **kwargs):
|
||||
cleanup_calls.append((args, kwargs))
|
||||
return 1, []
|
||||
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.ConnectorService,
|
||||
"cleanup_stale_documents_for_task",
|
||||
lambda *_args, **_kwargs: cleanup_calls.append((_args, _kwargs)),
|
||||
_fake_cleanup,
|
||||
)
|
||||
|
||||
await _FakeSync((iter(()), []))._run_task_logic(_make_task())
|
||||
|
||||
assert cleanup_calls == []
|
||||
assert cleanup_calls == [
|
||||
(
|
||||
(
|
||||
"task-1",
|
||||
"connector-1",
|
||||
"kb-1",
|
||||
"tenant-1",
|
||||
[],
|
||||
),
|
||||
{},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -170,6 +198,203 @@ async def test_run_task_logic_cleans_up_for_non_empty_snapshot(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
class _FakeRDBMSConnector:
|
||||
instance = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db_type,
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
query,
|
||||
content_columns,
|
||||
metadata_columns=None,
|
||||
id_column=None,
|
||||
timestamp_column=None,
|
||||
batch_size=2,
|
||||
):
|
||||
self.db_type = db_type
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.database = database
|
||||
self.query = query
|
||||
self.content_columns = content_columns
|
||||
self.metadata_columns = metadata_columns
|
||||
self.id_column = id_column
|
||||
self.timestamp_column = timestamp_column
|
||||
self.batch_size = batch_size
|
||||
self.load_from_state_called = False
|
||||
self.retrieve_all_slim_docs_perm_sync_called = False
|
||||
self.prepare_sync_state_called = False
|
||||
self.load_from_cursor_range_called = False
|
||||
self.persist_sync_state_called = False
|
||||
self._pending_sync_cursor_value = None
|
||||
_FakeRDBMSConnector.instance = self
|
||||
|
||||
def load_credentials(self, credentials):
|
||||
self.credentials = credentials
|
||||
|
||||
def validate_connector_settings(self):
|
||||
return None
|
||||
|
||||
def prepare_sync_state(self, connector_id, config):
|
||||
self.prepare_sync_state_called = True
|
||||
self.prepare_sync_state_args = (connector_id, config)
|
||||
|
||||
def get_saved_sync_cursor_value(self):
|
||||
return None
|
||||
|
||||
def retrieve_all_slim_docs_perm_sync(self, callback=None):
|
||||
del callback
|
||||
self.retrieve_all_slim_docs_perm_sync_called = True
|
||||
yield [types.SimpleNamespace(id="row-1")]
|
||||
|
||||
def load_from_state(self):
|
||||
self.load_from_state_called = True
|
||||
return iter((["full-sync"],))
|
||||
|
||||
def load_from_cursor_range(self, start_value=None, end_value=None):
|
||||
self.load_from_cursor_range_called = True
|
||||
return iter(([ _make_fake_doc("incremental-doc") ],))
|
||||
|
||||
def persist_sync_state(self):
|
||||
self.persist_sync_state_called = True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.p2
|
||||
async def test_rdbms_generate_keeps_deleted_file_snapshot_without_timestamp_column(monkeypatch):
|
||||
monkeypatch.setattr(sync_data_source, "RDBMSConnector", _FakeRDBMSConnector)
|
||||
|
||||
task = {
|
||||
**_make_task(),
|
||||
"reindex": "0",
|
||||
"poll_range_start": datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
"skip_connection_log": True,
|
||||
}
|
||||
sync = sync_data_source.MySQL(
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "db",
|
||||
"query": "SELECT * FROM t",
|
||||
"content_columns": "name",
|
||||
"credentials": {"username": "u", "password": "p"},
|
||||
"sync_deleted_files": True,
|
||||
}
|
||||
)
|
||||
|
||||
document_generator, file_list = await sync._generate(task)
|
||||
connector = _FakeRDBMSConnector.instance
|
||||
|
||||
assert connector is not None
|
||||
assert connector.load_from_state_called is True
|
||||
assert connector.load_from_cursor_range_called is False
|
||||
assert connector.retrieve_all_slim_docs_perm_sync_called is True
|
||||
assert file_list is not None
|
||||
assert [doc.id for doc in file_list] == ["row-1"]
|
||||
assert list(document_generator) == [["full-sync"]]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.p2
|
||||
async def test_rdbms_cursor_persists_only_after_success(monkeypatch):
|
||||
monkeypatch.setattr(sync_data_source, "RDBMSConnector", _FakeRDBMSConnector)
|
||||
_patch_common_dependencies(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.KnowledgebaseService,
|
||||
"get_by_id",
|
||||
lambda *_args, **_kwargs: (True, object()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.SyncLogsService,
|
||||
"increase_docs",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.SyncLogsService,
|
||||
"duplicate_and_parse",
|
||||
lambda *_args, **_kwargs: ([], ["parsed-doc-id"]),
|
||||
)
|
||||
|
||||
task = {
|
||||
**_make_task(),
|
||||
"reindex": "0",
|
||||
"poll_range_start": datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
"skip_connection_log": True,
|
||||
}
|
||||
sync = sync_data_source.MySQL(
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "db",
|
||||
"query": "SELECT * FROM t",
|
||||
"content_columns": "name",
|
||||
"timestamp_column": "ts",
|
||||
"credentials": {"username": "u", "password": "p"},
|
||||
"sync_deleted_files": False,
|
||||
}
|
||||
)
|
||||
|
||||
await sync._run_task_logic(task)
|
||||
|
||||
connector = _FakeRDBMSConnector.instance
|
||||
assert connector is not None
|
||||
assert connector.persist_sync_state_called is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.p2
|
||||
async def test_rdbms_cursor_does_not_persist_when_batch_is_skipped(monkeypatch):
|
||||
monkeypatch.setattr(sync_data_source, "RDBMSConnector", _FakeRDBMSConnector)
|
||||
_patch_common_dependencies(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.KnowledgebaseService,
|
||||
"get_by_id",
|
||||
lambda *_args, **_kwargs: (True, object()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.SyncLogsService,
|
||||
"increase_docs",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
def _raise_in_duplicate_and_parse(*_args, **_kwargs):
|
||||
raise RuntimeError("batch failed")
|
||||
|
||||
monkeypatch.setattr(
|
||||
sync_data_source.SyncLogsService,
|
||||
"duplicate_and_parse",
|
||||
_raise_in_duplicate_and_parse,
|
||||
)
|
||||
|
||||
task = {
|
||||
**_make_task(),
|
||||
"reindex": "0",
|
||||
"poll_range_start": datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
"skip_connection_log": True,
|
||||
}
|
||||
sync = sync_data_source.MySQL(
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "db",
|
||||
"query": "SELECT * FROM t",
|
||||
"content_columns": "name",
|
||||
"timestamp_column": "ts",
|
||||
"credentials": {"username": "u", "password": "p"},
|
||||
"sync_deleted_files": False,
|
||||
}
|
||||
)
|
||||
|
||||
await sync._run_task_logic(task)
|
||||
|
||||
connector = _FakeRDBMSConnector.instance
|
||||
assert connector is not None
|
||||
assert connector.persist_sync_state_called is False
|
||||
|
||||
|
||||
class _FakeDropboxConnector:
|
||||
instance = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user