feat: Add SQLite database support and fixed portable extraction path
- Add SQLite as alternative database for offline/firewall environments - Add --database-type parameter to build-client.bat (mysql/sqlite) - Refactor database.py to support both MySQL and SQLite - Add DB_TYPE and SQLITE_PATH configuration options - Set fixed unpackDirName for portable exe (Meeting-Assistant) - Update DEPLOYMENT.md with SQLite mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,59 @@
|
||||
"""
|
||||
Database abstraction layer supporting both MySQL and SQLite.
|
||||
|
||||
Usage:
|
||||
from app.database import init_db, get_db_cursor, init_tables
|
||||
|
||||
# At application startup
|
||||
init_db()
|
||||
init_tables()
|
||||
|
||||
# In request handlers
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute("SELECT * FROM meeting_records")
|
||||
results = cursor.fetchall()
|
||||
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute("INSERT INTO ...")
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
import mysql.connector
|
||||
from mysql.connector import pooling
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .config import settings
|
||||
|
||||
connection_pool = None
|
||||
# Global state
|
||||
_db_type: str = "mysql"
|
||||
_mysql_pool = None
|
||||
_sqlite_conn = None
|
||||
_sqlite_lock = threading.Lock()
|
||||
|
||||
|
||||
def init_db_pool():
|
||||
global connection_pool
|
||||
connection_pool = pooling.MySQLConnectionPool(
|
||||
# ============================================================================
|
||||
# Initialization Functions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database based on DB_TYPE setting."""
|
||||
global _db_type
|
||||
_db_type = settings.DB_TYPE.lower()
|
||||
|
||||
if _db_type == "sqlite":
|
||||
init_sqlite()
|
||||
else:
|
||||
init_mysql()
|
||||
|
||||
|
||||
def init_mysql():
|
||||
"""Initialize MySQL connection pool."""
|
||||
global _mysql_pool
|
||||
_mysql_pool = pooling.MySQLConnectionPool(
|
||||
pool_name="meeting_pool",
|
||||
pool_size=settings.DB_POOL_SIZE,
|
||||
host=settings.DB_HOST,
|
||||
@@ -17,80 +62,250 @@ def init_db_pool():
|
||||
password=settings.DB_PASS,
|
||||
database=settings.DB_NAME,
|
||||
)
|
||||
return connection_pool
|
||||
return _mysql_pool
|
||||
|
||||
|
||||
def init_sqlite():
|
||||
"""Initialize SQLite connection with row_factory for dict-like access."""
|
||||
global _sqlite_conn
|
||||
|
||||
db_path = settings.get_sqlite_path()
|
||||
db_dir = os.path.dirname(db_path)
|
||||
|
||||
# Create directory if needed
|
||||
if db_dir and not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
_sqlite_conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||
_sqlite_conn.row_factory = sqlite3.Row
|
||||
_sqlite_conn.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
print(f"SQLite database initialized at: {db_path}", flush=True)
|
||||
return _sqlite_conn
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Legacy Compatibility
|
||||
# ============================================================================
|
||||
|
||||
def init_db_pool():
|
||||
"""Legacy function for backward compatibility. Use init_db() instead."""
|
||||
return init_db()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Connection Context Managers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_connection():
|
||||
conn = connection_pool.get_connection()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
"""Get a database connection (MySQL or SQLite)."""
|
||||
if _db_type == "sqlite":
|
||||
# SQLite uses a single connection with thread lock
|
||||
yield _sqlite_conn
|
||||
else:
|
||||
# MySQL uses connection pool
|
||||
conn = _mysql_pool.get_connection()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SQLiteCursorWrapper:
|
||||
"""Wrapper to make SQLite cursor behave more like MySQL cursor with dictionary=True."""
|
||||
|
||||
def __init__(self, cursor):
|
||||
self._cursor = cursor
|
||||
self.lastrowid = None
|
||||
self.rowcount = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
# Convert MySQL-style %s placeholders to SQLite ? placeholders
|
||||
query = query.replace("%s", "?")
|
||||
if params:
|
||||
self._cursor.execute(query, params)
|
||||
else:
|
||||
self._cursor.execute(query)
|
||||
self.lastrowid = self._cursor.lastrowid
|
||||
self.rowcount = self._cursor.rowcount
|
||||
|
||||
def executemany(self, query, params_list):
|
||||
query = query.replace("%s", "?")
|
||||
self._cursor.executemany(query, params_list)
|
||||
self.lastrowid = self._cursor.lastrowid
|
||||
self.rowcount = self._cursor.rowcount
|
||||
|
||||
def fetchone(self):
|
||||
row = self._cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return dict(row)
|
||||
|
||||
def fetchall(self):
|
||||
rows = self._cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def fetchmany(self, size=None):
|
||||
if size:
|
||||
rows = self._cursor.fetchmany(size)
|
||||
else:
|
||||
rows = self._cursor.fetchmany()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def close(self):
|
||||
self._cursor.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_cursor(commit=False):
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
try:
|
||||
yield cursor
|
||||
if commit:
|
||||
conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
"""Get a database cursor that returns dict-like rows.
|
||||
|
||||
Args:
|
||||
commit: If True, commit the transaction after yield.
|
||||
|
||||
Yields:
|
||||
cursor: A cursor that returns dict-like rows.
|
||||
"""
|
||||
if _db_type == "sqlite":
|
||||
with _sqlite_lock:
|
||||
cursor = SQLiteCursorWrapper(_sqlite_conn.cursor())
|
||||
try:
|
||||
yield cursor
|
||||
if commit:
|
||||
_sqlite_conn.commit()
|
||||
except Exception:
|
||||
_sqlite_conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
else:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
try:
|
||||
yield cursor
|
||||
if commit:
|
||||
conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Table Initialization
|
||||
# ============================================================================
|
||||
|
||||
MYSQL_TABLES = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_users (
|
||||
user_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(50),
|
||||
role ENUM('admin', 'user') DEFAULT 'user',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_records (
|
||||
meeting_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
uuid VARCHAR(64) UNIQUE,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
meeting_time DATETIME NOT NULL,
|
||||
location VARCHAR(100),
|
||||
chairperson VARCHAR(50),
|
||||
recorder VARCHAR(50),
|
||||
attendees TEXT,
|
||||
transcript_blob LONGTEXT,
|
||||
created_by VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_conclusions (
|
||||
conclusion_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
meeting_id INT,
|
||||
content TEXT,
|
||||
system_code VARCHAR(20),
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_action_items (
|
||||
action_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
meeting_id INT,
|
||||
content TEXT,
|
||||
owner VARCHAR(50),
|
||||
due_date DATE,
|
||||
status ENUM('Open', 'In Progress', 'Done', 'Delayed') DEFAULT 'Open',
|
||||
system_code VARCHAR(20),
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
SQLITE_TABLES = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_users (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
role TEXT CHECK(role IN ('admin', 'user')) DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_records (
|
||||
meeting_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE,
|
||||
subject TEXT NOT NULL,
|
||||
meeting_time DATETIME NOT NULL,
|
||||
location TEXT,
|
||||
chairperson TEXT,
|
||||
recorder TEXT,
|
||||
attendees TEXT,
|
||||
transcript_blob TEXT,
|
||||
created_by TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_conclusions (
|
||||
conclusion_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
meeting_id INTEGER,
|
||||
content TEXT,
|
||||
system_code TEXT,
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_action_items (
|
||||
action_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
meeting_id INTEGER,
|
||||
content TEXT,
|
||||
owner TEXT,
|
||||
due_date DATE,
|
||||
status TEXT CHECK(status IN ('Open', 'In Progress', 'Done', 'Delayed')) DEFAULT 'Open',
|
||||
system_code TEXT,
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def init_tables():
|
||||
"""Create all required tables if they don't exist."""
|
||||
create_statements = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_users (
|
||||
user_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(50),
|
||||
role ENUM('admin', 'user') DEFAULT 'user',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_records (
|
||||
meeting_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
uuid VARCHAR(64) UNIQUE,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
meeting_time DATETIME NOT NULL,
|
||||
location VARCHAR(100),
|
||||
chairperson VARCHAR(50),
|
||||
recorder VARCHAR(50),
|
||||
attendees TEXT,
|
||||
transcript_blob LONGTEXT,
|
||||
created_by VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_conclusions (
|
||||
conclusion_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
meeting_id INT,
|
||||
content TEXT,
|
||||
system_code VARCHAR(20),
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_action_items (
|
||||
action_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
meeting_id INT,
|
||||
content TEXT,
|
||||
owner VARCHAR(50),
|
||||
due_date DATE,
|
||||
status ENUM('Open', 'In Progress', 'Done', 'Delayed') DEFAULT 'Open',
|
||||
system_code VARCHAR(20),
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
]
|
||||
tables = SQLITE_TABLES if _db_type == "sqlite" else MYSQL_TABLES
|
||||
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
for statement in create_statements:
|
||||
cursor.execute(statement)
|
||||
if _db_type == "sqlite":
|
||||
with _sqlite_lock:
|
||||
cursor = _sqlite_conn.cursor()
|
||||
try:
|
||||
for statement in tables:
|
||||
cursor.execute(statement)
|
||||
_sqlite_conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
else:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
for statement in tables:
|
||||
cursor.execute(statement)
|
||||
|
||||
Reference in New Issue
Block a user