Source code for pain001.db.load_db_data_streaming
# Copyright (C) 2023-2026 Pain001. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Licensed under either of the Apache License, Version 2.0 or the MIT
# License, at your option. You may not use this file except in
# compliance with one of those licences. Copies are provided in
# LICENSE-APACHE and LICENSE-MIT.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licences is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the applicable Licence for the specific language
# governing permissions and limitations.
"""Memory-efficient streaming loader for SQLite database."""
import os
from collections.abc import Generator
from typing import Any
from pain001.db.load_db_data import (
_connect_sqlite_read_only,
sanitize_table_name,
)
from pain001.exceptions import DataSourceError
from pain001.security import validate_path
[docs]
def load_db_data_streaming(
data_file_path: str, table_name: str, chunk_size: int = 1000
) -> Generator[list[dict[str, Any]], None, None]:
"""Load data from SQLite database in chunks for memory-efficient processing.
This function yields chunks of database rows instead of loading the entire
table into memory, making it suitable for large databases.
Args:
data_file_path: The path to the SQLite database file.
table_name: The name of the table from which data will be loaded.
chunk_size: Number of rows to yield per chunk. Default is 1000.
Yields:
list[dict[str, Any]]: Up to chunk_size rows of table data.
Raises:
FileNotFoundError: If the SQLite file does not exist or fails
path validation.
DataSourceError: If the table is empty or doesn't exist.
Example:
>>> for chunk in load_db_data_streaming('payments.db', 'pain001', chunk_size=500):
... # Process chunk
... process_payment_batch(chunk)
Performance:
- Memory usage: O(chunk_size) instead of O(table_rows)
- Enables processing of tables larger than available RAM
- Uses cursor fetching for efficient streaming
"""
try:
base_dir = os.getcwd()
safe_path = validate_path(
data_file_path,
must_exist=True,
base_dir=base_dir,
)
except Exception as exc:
raise FileNotFoundError(
f"SQLite file path validation failed: {data_file_path}"
) from exc
if not os.path.isfile(safe_path):
raise FileNotFoundError(
f"SQLite file '{data_file_path}' does not exist."
)
# Sanitize table name to prevent SQL injection
# Validate the table_name before using it in the query (strict regex validation)
table_name = sanitize_table_name(table_name)
# Connect to the SQLite database
conn = _connect_sqlite_read_only(safe_path)
try:
cursor = conn.cursor()
# Fetch column names from the table
# Safe: table_name validated via regex to contain only [a-zA-Z0-9_]
cursor.execute(f"PRAGMA table_info({table_name})") # nosec B608
columns_info = cursor.fetchall()
if not columns_info:
raise DataSourceError(
f"Table '{table_name}' does not exist or has no columns."
)
columns = [column[1] for column in columns_info]
# Use parameterized query to prevent SQL injection
# Note: SQLite does not support ? placeholders for table names.
# sanitize_table_name() enforces strict validation: ^[a-zA-Z][a-zA-Z0-9_]*$
query = f"SELECT * FROM [{table_name}]" # nosec B608
cursor.execute(query)
row_count = 0
while True:
# Fetch chunk_size rows
rows = cursor.fetchmany(chunk_size)
if not rows:
break
# Convert rows to list of dictionaries
chunk: list[dict[str, Any]] = []
for row in rows:
row_dict = {}
for i, value in enumerate(row):
row_dict[columns[i]] = value
chunk.append(row_dict)
row_count += 1
yield chunk
if row_count == 0:
raise DataSourceError(
f"The table '{table_name}' in '{data_file_path}' is empty."
)
finally:
# Close the connection to the SQLite database
conn.close()