fix: support sqlite
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
This commit is contained in:
parent
f8ccbfd3d7
commit
ad32216040
23 changed files with 359 additions and 164 deletions
|
|
@ -69,9 +69,10 @@ if TYPE_CHECKING: # pragma: no cover
|
||||||
from frappe.database.mariadb.database import MariaDBDatabase as PyMariaDBDatabase
|
from frappe.database.mariadb.database import MariaDBDatabase as PyMariaDBDatabase
|
||||||
from frappe.database.mariadb.mysqlclient import MariaDBDatabase
|
from frappe.database.mariadb.mysqlclient import MariaDBDatabase
|
||||||
from frappe.database.postgres.database import PostgresDatabase
|
from frappe.database.postgres.database import PostgresDatabase
|
||||||
|
from frappe.database.sqlite.database import SQLiteDatabase
|
||||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.builder import MariaDB, Postgres
|
from frappe.query_builder.builder import MariaDB, Postgres, SQLite
|
||||||
from frappe.types.lazytranslatedstring import _LazyTranslate
|
from frappe.types.lazytranslatedstring import _LazyTranslate
|
||||||
from frappe.utils.redis_wrapper import ClientCache, RedisWrapper
|
from frappe.utils.redis_wrapper import ClientCache, RedisWrapper
|
||||||
|
|
||||||
|
|
@ -161,8 +162,8 @@ ResponseDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||||
FlagsDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
FlagsDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||||
FormDict: TypeAlias = _dict[str, str]
|
FormDict: TypeAlias = _dict[str, str]
|
||||||
|
|
||||||
db: LocalProxy[Union["PyMariaDBDatabase", "MariaDBDatabase", "PostgresDatabase"]] = local("db")
|
db: LocalProxy[Union["PyMariaDBDatabase", "MariaDBDatabase", "PostgresDatabase", "SQLiteDatabase"]] = local("db")
|
||||||
qb: LocalProxy[Union["MariaDB", "Postgres"]] = local("qb")
|
qb: LocalProxy[Union["MariaDB", "Postgres", "SQLite"]] = local("qb")
|
||||||
conf: LocalProxy[ConfType] = local("conf")
|
conf: LocalProxy[ConfType] = local("conf")
|
||||||
form_dict: LocalProxy[FormDict] = local("form_dict")
|
form_dict: LocalProxy[FormDict] = local("form_dict")
|
||||||
form = form_dict
|
form = form_dict
|
||||||
|
|
@ -182,7 +183,7 @@ lang: LocalProxy[str] = local("lang")
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
# trick because some type checkers fail to follow "RedisWrapper", etc (written as string literal)
|
# trick because some type checkers fail to follow "RedisWrapper", etc (written as string literal)
|
||||||
# trough a generic wrapper; seems to be a bug
|
# trough a generic wrapper; seems to be a bug
|
||||||
db: PyMariaDBDatabase | MariaDBDatabase | PostgresDatabase
|
db: PyMariaDBDatabase | MariaDBDatabase | PostgresDatabase | SQLiteDatabase
|
||||||
qb: MariaDB | Postgres
|
qb: MariaDB | Postgres
|
||||||
conf: ConfType
|
conf: ConfType
|
||||||
form_dict: FormDict
|
form_dict: FormDict
|
||||||
|
|
|
||||||
|
|
@ -203,13 +203,24 @@ def build_table_count_cache():
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
table_name = frappe.qb.Field("table_name").as_("name")
|
if frappe.db.db_type != "sqlite":
|
||||||
table_rows = frappe.qb.Field("table_rows").as_("count")
|
table_name = frappe.qb.Field("table_name").as_("name")
|
||||||
information_schema = frappe.qb.Schema("information_schema")
|
table_rows = frappe.qb.Field("table_rows").as_("count")
|
||||||
|
information_schema = frappe.qb.Schema("information_schema")
|
||||||
|
|
||||||
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(as_dict=True)
|
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(as_dict=True)
|
||||||
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
|
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
|
||||||
frappe.cache.set_value("information_schema:counts", counts)
|
frappe.cache.set_value("information_schema:counts", counts)
|
||||||
|
else:
|
||||||
|
counts = {}
|
||||||
|
name = frappe.qb.Field("name")
|
||||||
|
type = frappe.qb.Field("type")
|
||||||
|
sqlite_master = frappe.qb.Schema("sqlite_master")
|
||||||
|
data = frappe.qb.from_(sqlite_master).select(name).where(type == "table").run(as_dict=True)
|
||||||
|
for table in data:
|
||||||
|
count = frappe.db.sql(f"SELECT COUNT(*) FROM `{table.name}`")[0][0]
|
||||||
|
counts[table.name.replace("tab", "", 1)] = count
|
||||||
|
frappe.cache.set_value("information_schema:counts", counts)
|
||||||
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -524,12 +524,27 @@ def postgres(context: CliCtxObj, extra_args):
|
||||||
_enter_console(extra_args=extra_args)
|
_enter_console(extra_args=extra_args)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("sqlite", context_settings=EXTRA_ARGS_CTX)
|
||||||
|
@click.argument("extra_args", nargs=-1)
|
||||||
|
@pass_context
|
||||||
|
def sqlite(context: CliCtxObj, extra_args):
|
||||||
|
"""
|
||||||
|
Enter into sqlite console for a given site.
|
||||||
|
"""
|
||||||
|
site = get_site(context)
|
||||||
|
frappe.init(site)
|
||||||
|
frappe.conf.db_type = "sqlite"
|
||||||
|
_enter_console(extra_args=extra_args)
|
||||||
|
|
||||||
|
|
||||||
def _enter_console(extra_args=None):
|
def _enter_console(extra_args=None):
|
||||||
from frappe.database import get_command
|
from frappe.database import get_command
|
||||||
from frappe.utils import get_site_path
|
from frappe.utils import get_site_path
|
||||||
|
|
||||||
if frappe.conf.db_type == "mariadb":
|
if frappe.conf.db_type == "mariadb":
|
||||||
os.environ["MYSQL_HISTFILE"] = os.path.abspath(get_site_path("logs", "mariadb_console.log"))
|
os.environ["MYSQL_HISTFILE"] = os.path.abspath(get_site_path("logs", "mariadb_console.log"))
|
||||||
|
elif frappe.conf.db_type == "sqlite":
|
||||||
|
os.environ["SQLITE_HISTORY"] = os.path.abspath(get_site_path("logs", "sqlite_console.log"))
|
||||||
else:
|
else:
|
||||||
os.environ["PSQL_HISTORY"] = os.path.abspath(get_site_path("logs", "postgresql_console.log"))
|
os.environ["PSQL_HISTORY"] = os.path.abspath(get_site_path("logs", "postgresql_console.log"))
|
||||||
|
|
||||||
|
|
@ -1033,6 +1048,7 @@ commands = [
|
||||||
make_app,
|
make_app,
|
||||||
create_patch,
|
create_patch,
|
||||||
mariadb,
|
mariadb,
|
||||||
|
sqlite,
|
||||||
postgres,
|
postgres,
|
||||||
request,
|
request,
|
||||||
reset_perms,
|
reset_perms,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,27 @@ def execute(filters=None):
|
||||||
WHERE table_schema = 'public'
|
WHERE table_schema = 'public'
|
||||||
ORDER BY 2 DESC;
|
ORDER BY 2 DESC;
|
||||||
""",
|
""",
|
||||||
|
"sqlite": """
|
||||||
|
WITH RECURSIVE
|
||||||
|
page_size AS (
|
||||||
|
SELECT CAST(page_size AS FLOAT) as size FROM PRAGMA_page_size()
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.name as 'table',
|
||||||
|
ROUND(CAST((SELECT SUM(pgsize) FROM dbstat WHERE name = m.name) * page_size.size / (1024.0 * 1024.0 * 1024.0) AS FLOAT), 2) as 'data_size_mb',
|
||||||
|
ROUND(CAST((SELECT SUM(pgsize) FROM dbstat WHERE name IN (
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type = 'index' AND tbl_name = m.name
|
||||||
|
)) * page_size.size / (1024.0 * 1024.0 * 1024.0) AS FLOAT), 2) as 'index_size_mb',
|
||||||
|
ROUND(CAST((SELECT SUM(pgsize) FROM dbstat WHERE name = m.name OR name IN (
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type = 'index' AND tbl_name = m.name
|
||||||
|
)) * page_size.size / (1024.0 * 1024.0 * 1024.0) AS FLOAT), 2) as 'total_size_mb'
|
||||||
|
FROM sqlite_master m
|
||||||
|
CROSS JOIN page_size
|
||||||
|
WHERE m.type = 'table'
|
||||||
|
AND m.name NOT LIKE 'sqlite_%'
|
||||||
|
ORDER BY total_size_mb DESC;""",
|
||||||
},
|
},
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
# Database Module
|
# Database Module
|
||||||
# --------------------
|
# --------------------
|
||||||
|
from pathlib import Path
|
||||||
from shutil import which
|
from shutil import which
|
||||||
|
|
||||||
from frappe.database.database import savepoint
|
from frappe.database.database import savepoint
|
||||||
|
|
@ -133,7 +134,10 @@ def get_command(
|
||||||
|
|
||||||
elif frappe.conf.db_type == "sqlite":
|
elif frappe.conf.db_type == "sqlite":
|
||||||
bin, bin_name = which("sqlite3"), "sqlite3"
|
bin, bin_name = which("sqlite3"), "sqlite3"
|
||||||
command = []
|
db_path = Path(frappe.get_site_path()) / "db" / f"{db_name}.db"
|
||||||
|
command = [db_path.as_posix()]
|
||||||
|
if dump:
|
||||||
|
command.append(".dump")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if dump:
|
if dump:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import re
|
||||||
import string
|
import string
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Hashable, Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from frappe import _
|
||||||
|
|
||||||
|
|
||||||
class DbManager:
|
class DbManager:
|
||||||
def __init__(self, db):
|
def __init__(self, db: frappe.database.database.Database | None = None):
|
||||||
"""
|
"""
|
||||||
Pass root_conn here for access to all databases.
|
Pass root_conn here for access to all databases.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -275,9 +275,9 @@ class DbColumn:
|
||||||
self.table.change_type.append(self)
|
self.table.change_type.append(self)
|
||||||
|
|
||||||
# unique
|
# unique
|
||||||
if (self.unique and not current_def["unique"]) and column_type not in ("text", "longtext"):
|
if (self.unique and not current_def.get("unique")) and column_type not in ("text", "longtext"):
|
||||||
self.table.add_unique.append(self)
|
self.table.add_unique.append(self)
|
||||||
elif (current_def["unique"] and not self.unique) and column_type not in ("text", "longtext"):
|
elif (current_def.get("unique") and not self.unique) and column_type not in ("text", "longtext"):
|
||||||
self.table.drop_unique.append(self)
|
self.table.drop_unique.append(self)
|
||||||
|
|
||||||
# default
|
# default
|
||||||
|
|
@ -289,21 +289,21 @@ class DbColumn:
|
||||||
self.table.set_default.append(self)
|
self.table.set_default.append(self)
|
||||||
|
|
||||||
# nullability
|
# nullability
|
||||||
if self.not_nullable is not None and (self.not_nullable != current_def["not_nullable"]):
|
if self.not_nullable is not None and (self.not_nullable != current_def.get("not_nullable")):
|
||||||
self.table.change_nullability.append(self)
|
self.table.change_nullability.append(self)
|
||||||
|
|
||||||
# index should be applied or dropped irrespective of type change
|
# index should be applied or dropped irrespective of type change
|
||||||
if (current_def["index"] and not self.set_index) and column_type not in ("text", "longtext"):
|
if (current_def.get("index") and not self.set_index) and column_type not in ("text", "longtext"):
|
||||||
self.table.drop_index.append(self)
|
self.table.drop_index.append(self)
|
||||||
|
|
||||||
elif (not current_def["index"] and self.set_index) and column_type not in ("text", "longtext"):
|
elif (not current_def.get("index") and self.set_index) and column_type not in ("text", "longtext"):
|
||||||
self.table.add_index.append(self)
|
self.table.add_index.append(self)
|
||||||
|
|
||||||
def default_changed(self, current_def):
|
def default_changed(self, current_def):
|
||||||
if "decimal" in current_def["type"]:
|
if "decimal" in current_def["type"]:
|
||||||
return self.default_changed_for_decimal(current_def)
|
return self.default_changed_for_decimal(current_def)
|
||||||
else:
|
else:
|
||||||
cur_default = current_def["default"]
|
cur_default = current_def.get("default")
|
||||||
new_default = self.default
|
new_default = self.default
|
||||||
if cur_default == "NULL" or cur_default is None:
|
if cur_default == "NULL" or cur_default is None:
|
||||||
cur_default = None
|
cur_default = None
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from contextlib import contextmanager
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.database.database import Database
|
from frappe.database.database import TRANSACTION_DISABLED_MSG, Database, ImplicitCommitError, is_query_type
|
||||||
from frappe.database.sqlite.schema import SQLiteTable
|
from frappe.database.sqlite.schema import SQLiteTable
|
||||||
from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name
|
from frappe.utils import get_table_name
|
||||||
|
|
||||||
_PARAM_COMP = re.compile(r"%\([\w]*\)s")
|
_PARAM_COMP = re.compile(r"%\([\w]*\)s")
|
||||||
|
|
||||||
|
|
@ -89,23 +90,20 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
||||||
MAX_ROW_SIZE_LIMIT = None
|
MAX_ROW_SIZE_LIMIT = None
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
conn = self._get_connection()
|
conn = self.create_connection()
|
||||||
conn.isolation_level = None
|
conn.isolation_level = None
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def _get_connection(self):
|
|
||||||
"""Return SQLite connection object."""
|
|
||||||
return self.create_connection()
|
|
||||||
|
|
||||||
def create_connection(self):
|
def create_connection(self):
|
||||||
return sqlite3.connect(self.get_connection_settings())
|
db_path = self.get_db_path()
|
||||||
|
return sqlite3.connect(db_path)
|
||||||
|
|
||||||
|
def get_db_path(self):
|
||||||
|
return Path(frappe.get_site_path()) / "db" / f"{self.cur_db_name}.db"
|
||||||
|
|
||||||
def set_execution_timeout(self, seconds: int):
|
def set_execution_timeout(self, seconds: int):
|
||||||
self.sql(f"PRAGMA busy_timeout = {int(seconds) * 1000}")
|
self.sql(f"PRAGMA busy_timeout = {int(seconds) * 1000}")
|
||||||
|
|
||||||
def get_connection_settings(self) -> str:
|
|
||||||
return self.cur_db_name
|
|
||||||
|
|
||||||
def setup_type_map(self):
|
def setup_type_map(self):
|
||||||
self.db_type = "sqlite"
|
self.db_type = "sqlite"
|
||||||
self.type_map = {
|
self.type_map = {
|
||||||
|
|
@ -245,7 +243,7 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def has_index(self, table_name, index_name):
|
def has_index(self, table_name, index_name):
|
||||||
return self.sql(f"PRAGMA index_list(`{table_name}`)")
|
return self.sql(f"SELECT * FROM pragma_index_list(`{table_name}`) WHERE name = '{index_name}'")
|
||||||
|
|
||||||
def get_column_index(self, table_name: str, fieldname: str, unique: bool = False) -> frappe._dict | None:
|
def get_column_index(self, table_name: str, fieldname: str, unique: bool = False) -> frappe._dict | None:
|
||||||
"""Check if column exists for a specific fields in specified order."""
|
"""Check if column exists for a specific fields in specified order."""
|
||||||
|
|
@ -254,18 +252,32 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
||||||
index_info = self.sql(f"PRAGMA index_info(`{index['name']}`)", as_dict=True)
|
index_info = self.sql(f"PRAGMA index_info(`{index['name']}`)", as_dict=True)
|
||||||
if index_info and index_info[0]["name"] == fieldname:
|
if index_info and index_info[0]["name"] == fieldname:
|
||||||
return index
|
return index
|
||||||
return None
|
|
||||||
|
|
||||||
def add_index(self, doctype: str, fields: list, index_name: str | None = None):
|
def add_index(self, doctype: str, fields: list, index_name: str | None = None):
|
||||||
"""Creates an index with given fields if not already created."""
|
"""Creates an index with given fields if not already created."""
|
||||||
|
|
||||||
|
# We can't specify the length of the index in SQLite
|
||||||
|
fields = [re.sub(r"\(.*?\)", "", field) for field in fields]
|
||||||
|
|
||||||
index_name = index_name or self.get_index_name(fields)
|
index_name = index_name or self.get_index_name(fields)
|
||||||
table_name = get_table_name(doctype)
|
table_name = get_table_name(doctype)
|
||||||
if not self.has_index(table_name, index_name):
|
self.commit()
|
||||||
self.commit()
|
self.sql(f"CREATE INDEX IF NOT EXISTS `{index_name}` ON `{table_name}` ({', '.join(fields)})")
|
||||||
self.sql(f"CREATE INDEX `{index_name}` ON `{table_name}` ({', '.join(fields)})")
|
|
||||||
|
|
||||||
def add_unique(self, doctype, fields, constraint_name=None):
|
def add_unique(self, doctype, fields, constraint_name=None):
|
||||||
raise NotImplementedError("SQLite does not support adding unique constraints directly.")
|
"""Creates unique constraint on fields."""
|
||||||
|
if isinstance(fields, str):
|
||||||
|
fields = [fields]
|
||||||
|
if not constraint_name:
|
||||||
|
constraint_name = f"unique_{'_'.join(fields)}"
|
||||||
|
table_name = get_table_name(doctype)
|
||||||
|
|
||||||
|
columns = ", ".join(fields)
|
||||||
|
sql_create_unique = (
|
||||||
|
f"CREATE UNIQUE INDEX IF NOT EXISTS `{constraint_name}` ON `{table_name}` ({columns})"
|
||||||
|
)
|
||||||
|
self.commit() # commit before creating index
|
||||||
|
self.sql(sql_create_unique)
|
||||||
|
|
||||||
def updatedb(self, doctype, meta=None):
|
def updatedb(self, doctype, meta=None):
|
||||||
"""Syncs a `DocType` to the table."""
|
"""Syncs a `DocType` to the table."""
|
||||||
|
|
@ -307,7 +319,8 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
||||||
query = query % {x: f"'{y}'" for x, y in values.items()}
|
query = query % {x: f"'{y}'" for x, y in values.items()}
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
return self._cursor.execute(query, values)
|
|
||||||
|
return self._cursor.execute(query, values or ())
|
||||||
|
|
||||||
def sql(self, *args, **kwargs):
|
def sql(self, *args, **kwargs):
|
||||||
if args:
|
if args:
|
||||||
|
|
@ -318,6 +331,90 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
||||||
elif kwargs.get("query"):
|
elif kwargs.get("query"):
|
||||||
kwargs["query"] = modify_query(kwargs.get("query"))
|
kwargs["query"] = modify_query(kwargs.get("query"))
|
||||||
|
|
||||||
|
return super().sql(*args, **kwargs)
|
||||||
|
|
||||||
|
def begin(self, *, read_only=False):
|
||||||
|
read_only = read_only or frappe.flags.read_only
|
||||||
|
# mode = "READ ONLY" if read_only else ""
|
||||||
|
# TODO: support read_only
|
||||||
|
self.sql("BEGIN")
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
"""Commit current transaction. Calls SQL `COMMIT`."""
|
||||||
|
if not self._conn:
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
if self._disable_transaction_control:
|
||||||
|
warnings.warn(message=TRANSACTION_DISABLED_MSG, stacklevel=2)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.before_rollback.reset()
|
||||||
|
self.after_rollback.reset()
|
||||||
|
|
||||||
|
self.before_commit.run()
|
||||||
|
|
||||||
|
self._conn.commit()
|
||||||
|
self.transaction_writes = 0
|
||||||
|
self.begin() # explicitly start a new transaction
|
||||||
|
|
||||||
|
self.after_commit.run()
|
||||||
|
|
||||||
|
def rollback(self, *, save_point=None):
|
||||||
|
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
|
||||||
|
if not self._conn:
|
||||||
|
self.connect()
|
||||||
|
if save_point:
|
||||||
|
self.sql(f"rollback to savepoint {save_point}")
|
||||||
|
elif not self._disable_transaction_control:
|
||||||
|
self.before_commit.reset()
|
||||||
|
self.after_commit.reset()
|
||||||
|
|
||||||
|
self.before_rollback.run()
|
||||||
|
|
||||||
|
self._conn.rollback()
|
||||||
|
self.begin()
|
||||||
|
|
||||||
|
self.after_rollback.run()
|
||||||
|
else:
|
||||||
|
warnings.warn(message=TRANSACTION_DISABLED_MSG, stacklevel=2)
|
||||||
|
|
||||||
|
def get_db_table_columns(self, table) -> list[str]:
|
||||||
|
"""Return list of column names from given table."""
|
||||||
|
key = f"table_columns::{table}"
|
||||||
|
columns = frappe.client_cache.get_value(key)
|
||||||
|
if columns is None:
|
||||||
|
columns = self.sql(f"PRAGMA table_info(`{table}`)", as_dict=True)
|
||||||
|
columns = [col["name"] for col in columns]
|
||||||
|
|
||||||
|
if columns:
|
||||||
|
frappe.cache.set_value(key, columns)
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def check_implicit_commit(self, query: str):
|
||||||
|
if (
|
||||||
|
self.transaction_writes
|
||||||
|
and query
|
||||||
|
and is_query_type(
|
||||||
|
query,
|
||||||
|
("start", "alter", "drop", "create", "truncate", "vacuum", "attach", "detach"),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ImplicitCommitError("This statement can cause implicit commit", query)
|
||||||
|
|
||||||
|
def estimate_count(self, doctype: str):
|
||||||
|
"""Get estimated count of total rows in a table."""
|
||||||
|
from frappe.utils.data import cint
|
||||||
|
|
||||||
|
table = get_table_name(doctype)
|
||||||
|
try:
|
||||||
|
if count := self.sql(f"SELECT COUNT(*) FROM `{table}`"):
|
||||||
|
return cint(count[0][0])
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if not self.is_table_missing(e):
|
||||||
|
raise
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def modify_query(query):
|
def modify_query(query):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from pymysql.constants.ER import DUP_ENTRY
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.database.schema import DBTable
|
from frappe.database.schema import DBTable
|
||||||
|
|
@ -8,149 +6,136 @@ from frappe.utils.defaults import get_not_null_defaults
|
||||||
|
|
||||||
class SQLiteTable(DBTable):
|
class SQLiteTable(DBTable):
|
||||||
def create(self):
|
def create(self):
|
||||||
|
# First prepare the basic table creation without indexes
|
||||||
additional_definitions = []
|
additional_definitions = []
|
||||||
engine = self.meta.get("engine") or "InnoDB"
|
name_column = "name TEXT PRIMARY KEY"
|
||||||
varchar_len = frappe.db.VARCHAR_LEN
|
|
||||||
name_column = f"name varchar({varchar_len}) primary key"
|
|
||||||
|
|
||||||
# columns
|
# columns
|
||||||
column_defs = self.get_column_definitions()
|
column_defs = self.get_column_definitions()
|
||||||
if column_defs:
|
if column_defs:
|
||||||
additional_definitions += column_defs
|
additional_definitions += column_defs
|
||||||
|
|
||||||
# index
|
index_defs = [] # Store index definitions separately
|
||||||
index_defs = self.get_index_definitions()
|
|
||||||
if index_defs:
|
|
||||||
additional_definitions += index_defs
|
|
||||||
|
|
||||||
# child table columns
|
# child table columns
|
||||||
if self.meta.get("istable", default=0):
|
if self.meta.get("istable", default=0):
|
||||||
additional_definitions += [
|
additional_definitions.extend(["parent TEXT", "parentfield TEXT", "parenttype TEXT"])
|
||||||
f"parent varchar({varchar_len})",
|
index_defs.append(f"CREATE INDEX `{self.table_name}_parent_idx` ON `{self.table_name}`(parent)")
|
||||||
f"parentfield varchar({varchar_len})",
|
|
||||||
f"parenttype varchar({varchar_len})",
|
|
||||||
"index parent(parent)",
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
# parent types
|
# parent types
|
||||||
additional_definitions.append("index creation(creation)")
|
index_defs.append(
|
||||||
|
f"CREATE INDEX `{self.table_name}_creation_idx` ON `{self.table_name}`(creation)"
|
||||||
|
)
|
||||||
if self.meta.sort_field == "modified":
|
if self.meta.sort_field == "modified":
|
||||||
# Support old doctype default by indexing it, also 2nd popular choice.
|
index_defs.append(
|
||||||
additional_definitions.append("index modified(modified)")
|
f"CREATE INDEX `{self.table_name}_modified_idx` ON `{self.table_name}`(modified)"
|
||||||
|
)
|
||||||
|
|
||||||
# creating sequence(s)
|
# creating sequence(s)
|
||||||
if not self.meta.issingle and self.meta.autoname == "autoincrement":
|
if not self.meta.issingle and self.meta.autoname == "autoincrement":
|
||||||
frappe.db.create_sequence(self.doctype, check_not_exists=True)
|
name_column = "name INTEGER PRIMARY KEY AUTOINCREMENT"
|
||||||
|
|
||||||
# NOTE: not used nextval func as default as the ability to restore
|
|
||||||
# database with sequences has bugs in mariadb and gives a scary error.
|
|
||||||
# issue link: https://jira.mariadb.org/browse/MDEV-20070
|
|
||||||
name_column = "name bigint primary key"
|
|
||||||
|
|
||||||
elif not self.meta.issingle and self.meta.autoname == "UUID":
|
elif not self.meta.issingle and self.meta.autoname == "UUID":
|
||||||
name_column = "name uuid primary key"
|
name_column = "name TEXT PRIMARY KEY"
|
||||||
|
|
||||||
additional_definitions = ",\n".join(additional_definitions)
|
additional_definitions = ",\n".join(additional_definitions)
|
||||||
|
|
||||||
# create table
|
# create table
|
||||||
query = f"""create table `{self.table_name}` (
|
create_table_query = f"""CREATE TABLE `{self.table_name}` (
|
||||||
{name_column},
|
{name_column},
|
||||||
creation datetime(6),
|
creation DATETIME,
|
||||||
modified datetime(6),
|
modified DATETIME,
|
||||||
modified_by varchar({varchar_len}),
|
modified_by TEXT,
|
||||||
owner varchar({varchar_len}),
|
owner TEXT,
|
||||||
docstatus tinyint not null default '0',
|
docstatus INTEGER NOT NULL DEFAULT 0,
|
||||||
idx int not null default '0',
|
idx INTEGER NOT NULL DEFAULT 0,
|
||||||
{additional_definitions})
|
{additional_definitions})"""
|
||||||
ENGINE={engine}
|
|
||||||
ROW_FORMAT=DYNAMIC
|
|
||||||
CHARACTER SET=utf8mb4
|
|
||||||
COLLATE=utf8mb4_unicode_ci"""
|
|
||||||
|
|
||||||
frappe.db.sql_ddl(query)
|
# Execute table creation
|
||||||
|
frappe.db.sql_ddl(create_table_query)
|
||||||
|
|
||||||
|
# Create indexes separately
|
||||||
|
for index_query in index_defs:
|
||||||
|
frappe.db.sql_ddl(index_query)
|
||||||
|
|
||||||
def alter(self):
|
def alter(self):
|
||||||
for col in self.columns.values():
|
for col in self.columns.values():
|
||||||
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
|
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
|
||||||
|
|
||||||
add_column_query = [f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column]
|
for col in self.add_column:
|
||||||
|
frappe.db.sql_ddl(
|
||||||
|
f"ALTER TABLE `{self.table_name}` ADD COLUMN `{col.fieldname}` {col.get_definition()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
self.change_type
|
||||||
|
or self.set_default
|
||||||
|
or self.change_nullability
|
||||||
|
or self.add_index
|
||||||
|
or self.add_unique
|
||||||
|
or self.drop_index
|
||||||
|
or self.drop_unique
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current table column definitions
|
||||||
|
existing_columns = []
|
||||||
|
for column in frappe.db.sql(f"PRAGMA table_info(`{self.table_name}`)", as_dict=1):
|
||||||
|
existing_columns.append(f"`{column.name}` {column.type}")
|
||||||
|
|
||||||
|
columns = existing_columns.copy()
|
||||||
|
|
||||||
|
# Modify existing columns
|
||||||
columns_to_modify = set(self.change_type + self.set_default + self.change_nullability)
|
columns_to_modify = set(self.change_type + self.set_default + self.change_nullability)
|
||||||
modify_column_query = [
|
for col in columns_to_modify:
|
||||||
f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
|
# Replace the old column definition with the new one
|
||||||
for col in columns_to_modify
|
for i, column in enumerate(columns):
|
||||||
]
|
if column.startswith(f"`{col.fieldname}`"):
|
||||||
if alter_pk := self.alter_primary_key():
|
columns[i] = f"`{col.fieldname}` {col.get_definition(for_modification=True)}"
|
||||||
modify_column_query.append(alter_pk)
|
break
|
||||||
|
|
||||||
modify_column_query.extend(
|
# Create new table
|
||||||
[f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)" for col in self.add_unique]
|
temp_table = f"{self.table_name}_new"
|
||||||
)
|
create_table = f"CREATE TABLE `{temp_table}` (\n{','.join(columns)}\n)"
|
||||||
add_index_query = [
|
frappe.db.sql_ddl(create_table)
|
||||||
f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)"
|
|
||||||
for col in self.add_index
|
|
||||||
if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
# Copy data
|
||||||
|
existing_columns = [col.split()[0] for col in existing_columns]
|
||||||
|
column_list = ", ".join(existing_columns)
|
||||||
|
frappe.db.sql_ddl(f"INSERT INTO `{temp_table}` SELECT {column_list} FROM `{self.table_name}`")
|
||||||
|
|
||||||
|
# Drop old table
|
||||||
|
frappe.db.sql_ddl(f"DROP TABLE `{self.table_name}`")
|
||||||
|
|
||||||
|
# Rename new table
|
||||||
|
frappe.db.sql_ddl(f"ALTER TABLE `{temp_table}` RENAME TO `{self.table_name}`")
|
||||||
|
|
||||||
|
# Recreate indexes
|
||||||
|
index_queries = []
|
||||||
|
if self.add_unique:
|
||||||
|
index_queries.extend(
|
||||||
|
f"CREATE UNIQUE INDEX `{col.fieldname}` ON `{self.table_name}` (`{col.fieldname}`)"
|
||||||
|
for col in self.add_unique
|
||||||
|
)
|
||||||
|
if self.add_index:
|
||||||
|
index_queries.extend(
|
||||||
|
f"CREATE INDEX `{col.fieldname}_index` ON `{self.table_name}` (`{col.fieldname}`)"
|
||||||
|
for col in self.add_index
|
||||||
|
if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False)
|
||||||
|
)
|
||||||
if self.meta.sort_field == "modified" and not frappe.db.get_column_index(
|
if self.meta.sort_field == "modified" and not frappe.db.get_column_index(
|
||||||
self.table_name, "modified", unique=False
|
self.table_name, "modified", unique=False
|
||||||
):
|
):
|
||||||
add_index_query.append("ADD INDEX `modified`(`modified`)")
|
index_queries.append(f"CREATE INDEX `modified` ON `{self.table_name}` (`modified`)")
|
||||||
|
|
||||||
drop_index_query = []
|
for query in index_queries:
|
||||||
|
frappe.db.sql_ddl(query)
|
||||||
for col in {*self.drop_index, *self.drop_unique}:
|
|
||||||
if col.fieldname == "name":
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_column = self.current_columns.get(col.fieldname.lower())
|
|
||||||
unique_constraint_changed = current_column.unique != col.unique
|
|
||||||
if unique_constraint_changed and not col.unique:
|
|
||||||
if unique_index := frappe.db.get_column_index(self.table_name, col.fieldname, unique=True):
|
|
||||||
drop_index_query.append(f"DROP INDEX `{unique_index.Key_name}`")
|
|
||||||
|
|
||||||
index_constraint_changed = current_column.index != col.set_index
|
|
||||||
if index_constraint_changed and not col.set_index:
|
|
||||||
if index_record := frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
|
|
||||||
drop_index_query.append(f"DROP INDEX `{index_record.Key_name}`")
|
|
||||||
|
|
||||||
for col in self.change_nullability:
|
|
||||||
if col.not_nullable:
|
|
||||||
try:
|
|
||||||
table = frappe.qb.DocType(self.doctype)
|
|
||||||
frappe.qb.update(table).set(
|
|
||||||
col.fieldname, col.default or get_not_null_defaults(col.fieldtype)
|
|
||||||
).where(table[col.fieldname].isnull()).run()
|
|
||||||
except Exception:
|
|
||||||
print(f"Failed to update data in {self.table_name} for {col.fieldname}")
|
|
||||||
raise
|
|
||||||
try:
|
|
||||||
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
|
|
||||||
if query_parts:
|
|
||||||
query_body = ", ".join(query_parts)
|
|
||||||
query = f"ALTER TABLE `{self.table_name}` {query_body}"
|
|
||||||
# nosemgrep
|
|
||||||
frappe.db.sql_ddl(query)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars
|
|
||||||
print(f"Failed to alter schema using query: {query}")
|
|
||||||
|
|
||||||
if e.args[0] == DUP_ENTRY:
|
|
||||||
fieldname = str(e).split("'")[-2]
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"{0} field cannot be set as unique in {1}, as there are non-unique existing values"
|
|
||||||
).format(fieldname, self.table_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
raise
|
|
||||||
|
|
||||||
def alter_primary_key(self) -> str | None:
|
def alter_primary_key(self) -> str | None:
|
||||||
# If there are no values in table allow migrating to UUID from varchar
|
# If there are no values in table allow migrating to UUID from TEXT
|
||||||
autoname = self.meta.autoname
|
autoname = self.meta.autoname
|
||||||
if autoname == "UUID" and frappe.db.get_column_type(self.doctype, "name") != "uuid":
|
if autoname == "UUID" and frappe.db.get_column_type(self.doctype, "name") != "TEXT":
|
||||||
if not frappe.db.get_value(self.doctype, {}, order_by=None):
|
if not frappe.db.get_value(self.doctype, {}, order_by=None):
|
||||||
return "modify name uuid"
|
return "ALTER COLUMN name TEXT"
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Primary key of doctype {0} can not be changed as there are existing values.").format(
|
_("Primary key of doctype {0} can not be changed as there are existing values.").format(
|
||||||
|
|
@ -158,6 +143,6 @@ class SQLiteTable(DBTable):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reverting from UUID to VARCHAR
|
# Reverting from UUID to TEXT
|
||||||
if autoname != "UUID" and frappe.db.get_column_type(self.doctype, "name") == "uuid":
|
if autoname != "UUID" and frappe.db.get_column_type(self.doctype, "name") == "TEXT":
|
||||||
return f"modify name varchar({frappe.db.VARCHAR_LEN})"
|
return "ALTER COLUMN name TEXT"
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ def setup_database(force, verbose):
|
||||||
def bootstrap_database(verbose, source_sql=None):
|
def bootstrap_database(verbose, source_sql=None):
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
frappe.connect()
|
|
||||||
|
|
||||||
import_db_from_sql(source_sql, verbose)
|
import_db_from_sql(source_sql, verbose)
|
||||||
|
|
||||||
frappe.connect()
|
frappe.connect()
|
||||||
|
|
@ -32,7 +30,7 @@ def bootstrap_database(verbose, source_sql=None):
|
||||||
secho(
|
secho(
|
||||||
"Table 'tabDefaultValue' missing in the restored site. "
|
"Table 'tabDefaultValue' missing in the restored site. "
|
||||||
"This happens when the backup fails to restore. Please check that the file is valid\n"
|
"This happens when the backup fails to restore. Please check that the file is valid\n"
|
||||||
"Do go through the above output to check the exact error message from MariaDB",
|
"Do go through the above output to check the exact error message",
|
||||||
fg="red",
|
fg="red",
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -44,9 +42,7 @@ def import_db_from_sql(source_sql=None, verbose=False):
|
||||||
db_name = frappe.conf.db_name
|
db_name = frappe.conf.db_name
|
||||||
if not source_sql:
|
if not source_sql:
|
||||||
source_sql = os.path.join(os.path.dirname(__file__), "framework_sqlite.sql")
|
source_sql = os.path.join(os.path.dirname(__file__), "framework_sqlite.sql")
|
||||||
DbManager(frappe.local.db).restore_database(
|
DbManager().restore_database(verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password)
|
||||||
verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password
|
|
||||||
)
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Imported from database {}".format(source_sql))
|
print("Imported from database {}".format(source_sql))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ def show_processlist():
|
||||||
|
|
||||||
|
|
||||||
def _show_processlist():
|
def _show_processlist():
|
||||||
|
if frappe.db.db_type == "sqlite":
|
||||||
|
return []
|
||||||
|
|
||||||
return frappe.db.multisql(
|
return frappe.db.multisql(
|
||||||
{
|
{
|
||||||
"postgres": """
|
"postgres": """
|
||||||
|
|
|
||||||
|
|
@ -231,10 +231,25 @@ class SystemHealthReport(Document):
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
sqlite_query = """
|
||||||
|
SELECT scheduled_job_type,
|
||||||
|
AVG(CASE WHEN status != 'Complete' THEN 1 ELSE 0 END) * 100 AS failure_rate
|
||||||
|
FROM `tabScheduled Job Log`
|
||||||
|
WHERE
|
||||||
|
creation > %(lower_threshold)s
|
||||||
|
AND modified > %(lower_threshold)s
|
||||||
|
AND creation < %(upper_threshold)s
|
||||||
|
GROUP BY scheduled_job_type
|
||||||
|
HAVING failure_rate > 0
|
||||||
|
ORDER BY failure_rate DESC
|
||||||
|
LIMIT 5
|
||||||
|
"""
|
||||||
|
|
||||||
failing_jobs = frappe.db.multisql(
|
failing_jobs = frappe.db.multisql(
|
||||||
{
|
{
|
||||||
"mariadb": mariadb_query,
|
"mariadb": mariadb_query,
|
||||||
"postgres": postgres_query,
|
"postgres": postgres_query,
|
||||||
|
"sqlite": sqlite_query,
|
||||||
},
|
},
|
||||||
{"lower_threshold": lower_threshold, "upper_threshold": upper_threshold},
|
{"lower_threshold": lower_threshold, "upper_threshold": upper_threshold},
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
|
|
|
||||||
|
|
@ -319,15 +319,32 @@ def get_communication_data(
|
||||||
{conditions}
|
{conditions}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return frappe.db.sql(
|
sqlite_query = f"""
|
||||||
"""
|
SELECT * FROM (
|
||||||
|
SELECT * FROM ({part1})
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM ({part2})
|
||||||
|
) AS combined
|
||||||
|
{group_by or ""}
|
||||||
|
ORDER BY communication_date DESC
|
||||||
|
LIMIT %(limit)s
|
||||||
|
OFFSET %(start)s"""
|
||||||
|
|
||||||
|
query = f"""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM (({part1}) UNION ({part2})) AS combined
|
FROM (({part1}) UNION ({part2})) AS combined
|
||||||
{group_by}
|
{group_by or ""}
|
||||||
ORDER BY communication_date DESC
|
ORDER BY communication_date DESC
|
||||||
LIMIT %(limit)s
|
LIMIT %(limit)s
|
||||||
OFFSET %(start)s
|
OFFSET %(start)s
|
||||||
""".format(part1=part1, part2=part2, group_by=(group_by or "")),
|
"""
|
||||||
|
|
||||||
|
return frappe.db.multisql(
|
||||||
|
{
|
||||||
|
"sqlite": sqlite_query,
|
||||||
|
"postgres": query,
|
||||||
|
"mariadb": query,
|
||||||
|
},
|
||||||
dict(
|
dict(
|
||||||
doctype=doctype,
|
doctype=doctype,
|
||||||
name=name,
|
name=name,
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ def get_countries_and_currencies():
|
||||||
symbol=country.currency_symbol,
|
symbol=country.currency_symbol,
|
||||||
fraction_units=country.currency_fraction_units,
|
fraction_units=country.currency_fraction_units,
|
||||||
smallest_currency_fraction_value=country.smallest_currency_fraction_value,
|
smallest_currency_fraction_value=country.smallest_currency_fraction_value,
|
||||||
number_format=country.number_format,
|
number_format=frappe.db.escape(country.number_format)[1:-1],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1180,8 +1180,7 @@ def cast_name(column: str) -> str:
|
||||||
Example:
|
Example:
|
||||||
input - "ifnull(`tabBlog Post`.`name`, '')=''"
|
input - "ifnull(`tabBlog Post`.`name`, '')=''"
|
||||||
output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """
|
output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """
|
||||||
|
if frappe.db.db_type != "postgres":
|
||||||
if frappe.db.db_type == "mariadb":
|
|
||||||
return column
|
return column
|
||||||
|
|
||||||
kwargs = {"string": column}
|
kwargs = {"string": column}
|
||||||
|
|
|
||||||
|
|
@ -232,11 +232,14 @@ class Document(BaseDocument, DocRef):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not is_doctype and isinstance(self.name, str):
|
if not is_doctype and isinstance(self.name, str):
|
||||||
|
for_update = ""
|
||||||
|
if self.flags.for_update and frappe.db.db_type != "sqlite":
|
||||||
|
for_update = "FOR UPDATE"
|
||||||
# Fast path - use raw SQL to avoid QB/ORM overheads.
|
# Fast path - use raw SQL to avoid QB/ORM overheads.
|
||||||
d = frappe.db.sql(
|
d = frappe.db.sql(
|
||||||
"SELECT * FROM {table_name} WHERE `name` = %s {for_update}".format(
|
"SELECT * FROM {table_name} WHERE `name` = %s {for_update}".format(
|
||||||
table_name=get_table_name(self.doctype, wrap_in_backticks=True),
|
table_name=get_table_name(self.doctype, wrap_in_backticks=True),
|
||||||
for_update="FOR UPDATE" if self.flags.for_update else "",
|
for_update=for_update,
|
||||||
),
|
),
|
||||||
(self.name),
|
(self.name),
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
|
|
@ -289,6 +292,9 @@ class Document(BaseDocument, DocRef):
|
||||||
for_update=self.flags.for_update,
|
for_update=self.flags.for_update,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
for_update = ""
|
||||||
|
if self.flags.for_update and frappe.db.db_type != "sqlite":
|
||||||
|
for_update = "FOR UPDATE"
|
||||||
# Fast pass for all other doctypes - using raw SQL
|
# Fast pass for all other doctypes - using raw SQL
|
||||||
children = frappe.db.sql(
|
children = frappe.db.sql(
|
||||||
"""SELECT * FROM {table_name}
|
"""SELECT * FROM {table_name}
|
||||||
|
|
@ -297,7 +303,7 @@ class Document(BaseDocument, DocRef):
|
||||||
AND `parentfield`= %(parentfield)s
|
AND `parentfield`= %(parentfield)s
|
||||||
ORDER BY `idx` ASC {for_update}""".format(
|
ORDER BY `idx` ASC {for_update}""".format(
|
||||||
table_name=get_table_name(child_doctype, wrap_in_backticks=True),
|
table_name=get_table_name(child_doctype, wrap_in_backticks=True),
|
||||||
for_update="FOR UPDATE" if self.flags.for_update else "",
|
for_update=for_update,
|
||||||
),
|
),
|
||||||
{"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname},
|
{"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname},
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ def sync_user_settings():
|
||||||
"postgres": """INSERT INTO `__UserSettings` (`user`, `doctype`, `data`)
|
"postgres": """INSERT INTO `__UserSettings` (`user`, `doctype`, `data`)
|
||||||
VALUES (%s, %s, %s)
|
VALUES (%s, %s, %s)
|
||||||
ON CONFLICT ("user", "doctype") DO UPDATE SET `data`=%s""",
|
ON CONFLICT ("user", "doctype") DO UPDATE SET `data`=%s""",
|
||||||
|
"sqlite": """INSERT OR REPLACE INTO `__UserSettings` (`user`, `doctype`, `data`)
|
||||||
|
VALUES (%s, %s, %s)""",
|
||||||
},
|
},
|
||||||
(user, doctype, data, data),
|
(user, doctype, data, data),
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder, SQLLiteQu
|
||||||
from pypika.queries import QueryBuilder, Schema, Table
|
from pypika.queries import QueryBuilder, Schema, Table
|
||||||
from pypika.terms import Function
|
from pypika.terms import Function
|
||||||
|
|
||||||
from frappe.query_builder.terms import ParameterizedValueWrapper
|
from frappe.query_builder.terms import ParameterizedValueWrapper, SQLiteParameterizedValueWrapper
|
||||||
from frappe.utils import get_table_name
|
from frappe.utils import get_table_name
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,11 +100,13 @@ class Postgres(Base, PostgreSQLQuery):
|
||||||
|
|
||||||
|
|
||||||
class SQLite(Base, SQLLiteQuery):
|
class SQLite(Base, SQLLiteQuery):
|
||||||
|
Field = terms.Field
|
||||||
|
|
||||||
_BuilderClasss = SQLLiteQueryBuilder
|
_BuilderClasss = SQLLiteQueryBuilder
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _builder(cls, *args, **kwargs) -> "SQLLiteQueryBuilder":
|
def _builder(cls, *args, **kwargs) -> "SQLLiteQueryBuilder":
|
||||||
return super()._builder(*args, wrapper_cls=ParameterizedValueWrapper, **kwargs)
|
return super()._builder(*args, wrapper_cls=SQLiteParameterizedValueWrapper, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_(cls, table, *args, **kwargs):
|
def from_(cls, table, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from pypika.dialects import SQLLiteValueWrapper
|
||||||
from pypika.queries import QueryBuilder
|
from pypika.queries import QueryBuilder
|
||||||
from pypika.terms import Criterion, Function, ValueWrapper
|
from pypika.terms import Criterion, Function, ValueWrapper
|
||||||
from pypika.utils import format_alias_sql
|
from pypika.utils import format_alias_sql
|
||||||
|
|
@ -71,6 +72,10 @@ class ParameterizedValueWrapper(ValueWrapper):
|
||||||
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
|
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteParameterizedValueWrapper(ParameterizedValueWrapper, SQLLiteValueWrapper):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ParameterizedFunction(Function):
|
class ParameterizedFunction(Function):
|
||||||
"""
|
"""
|
||||||
Class to monkey patch pypika.terms.Functions
|
Class to monkey patch pypika.terms.Functions
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,12 @@ class BackupGenerator:
|
||||||
elif self.backup_excludes:
|
elif self.backup_excludes:
|
||||||
extra.extend([f"--ignore-table={self.db_name}.{table}" for table in self.backup_excludes])
|
extra.extend([f"--ignore-table={self.db_name}.{table}" for table in self.backup_excludes])
|
||||||
|
|
||||||
|
elif self.db_type == "sqlite":
|
||||||
|
if self.backup_includes:
|
||||||
|
extra.extend([f'"{table}"' for table in self.backup_includes])
|
||||||
|
elif self.backup_excludes:
|
||||||
|
click.secho("Excluding tables is not supported for SQLite", fg="yellow")
|
||||||
|
|
||||||
elif self.db_type == "postgres":
|
elif self.db_type == "postgres":
|
||||||
if self.backup_includes:
|
if self.backup_includes:
|
||||||
extra.extend([f'--table=public."{table}"' for table in self.backup_includes])
|
extra.extend([f'--table=public."{table}"' for table in self.backup_includes])
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,9 @@ def insert_values_for_multiple_docs(all_contents):
|
||||||
(doctype, name, content, published, title, route)
|
(doctype, name, content, published, title, route)
|
||||||
VALUES {}
|
VALUES {}
|
||||||
ON CONFLICT("name", "doctype") DO NOTHING""".format(", ".join(batch_values)),
|
ON CONFLICT("name", "doctype") DO NOTHING""".format(", ".join(batch_values)),
|
||||||
|
"sqlite": """INSERT OR IGNORE INTO `__global_search`
|
||||||
|
(doctype, name, content, published, title, route)
|
||||||
|
VALUES {} """.format(", ".join(batch_values)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -447,6 +450,10 @@ def sync_value(value: dict):
|
||||||
`published`=%(published)s,
|
`published`=%(published)s,
|
||||||
`title`=%(title)s,
|
`title`=%(title)s,
|
||||||
`route`=%(route)s
|
`route`=%(route)s
|
||||||
|
""",
|
||||||
|
"sqlite": """INSERT OR REPLACE INTO `__global_search`
|
||||||
|
(`doctype`, `name`, `content`, `published`, `title`, `route`)
|
||||||
|
VALUES (%(doctype)s, %(name)s, %(content)s, %(published)s, %(title)s, %(route)s)
|
||||||
""",
|
""",
|
||||||
},
|
},
|
||||||
value,
|
value,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ def get_random(doctype: str, filters: dict | None = None, doc: bool = False):
|
||||||
"mariadb": f"""select name from `tab{doctype}` {condition}
|
"mariadb": f"""select name from `tab{doctype}` {condition}
|
||||||
order by RAND() limit 1 offset 0""",
|
order by RAND() limit 1 offset 0""",
|
||||||
"postgres": f"""select name from `tab{doctype}` {condition}
|
"postgres": f"""select name from `tab{doctype}` {condition}
|
||||||
|
order by RANDOM() limit 1 offset 0""",
|
||||||
|
"sqlite": f"""select name from `tab{doctype}` {condition}
|
||||||
order by RANDOM() limit 1 offset 0""",
|
order by RANDOM() limit 1 offset 0""",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue