feat: integer primary keys

This commit is contained in:
phot0n 2022-02-07 18:35:47 +05:30
parent 7ace873c3a
commit bebc8058b6
30 changed files with 431 additions and 59 deletions

View file

@ -35,6 +35,7 @@ from frappe.query_builder import (
patch_query_execute,
patch_query_aggregation,
)
from frappe.utils.data import cstr
__version__ = '14.0.0-dev'
@ -214,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
local.cache = {}
local.document_cache = {}
local.meta_cache = {}
local.autoincremented_doctypes = set()
local.form_dict = _dict()
local.session = _dict()
local.dev_server = _dev_server
@ -1001,7 +1003,7 @@ def get_module(modulename):
def scrub(txt):
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
return txt.replace(' ', '_').replace('-', '_').lower()
return cstr(txt).replace(' ', '_').replace('-', '_').lower()
def unscrub(txt):
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""

View file

@ -324,7 +324,7 @@ class DataExporter:
d = doc.copy()
meta = frappe.get_meta(dt)
if self.all_doctypes:
d.name = '"'+ d.name+'"'
d.name = f'"{d.name}"'
if len(rows) < rowidx + 1:
rows.append([""] * (len(self.columns) + 1))

View file

@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', {
frm.events.set_naming_rule_description(frm);
},
istable: (frm) => {
if (frm.doc.istable && frm.is_new()) {
frm.set_value('autoname', 'autoincrement');
frm.set_value('allow_rename', 0);
}
},
naming_rule: function(frm) {
// set the "autoname" property based on naming_rule
if (frm.doc.naming_rule && !frm.__from_autoname) {
@ -70,6 +77,10 @@ frappe.ui.form.on('DocType', {
if (frm.doc.naming_rule=='Set by user') {
frm.set_value('autoname', 'Prompt');
} else if (frm.doc.naming_rule === 'Autoincrement') {
frm.set_value('autoname', 'autoincrement');
// set allow rename to be false when using autoincrement
frm.set_value('allow_rename', 0);
} else if (frm.doc.naming_rule=='By fieldname') {
frm.set_value('autoname', 'field:');
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
@ -91,6 +102,7 @@ frappe.ui.form.on('DocType', {
set_naming_rule_description(frm) {
let naming_rule_description = {
'Set by user': '',
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>',
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist',
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist',
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', {
frm.__from_autoname = true;
if (frm.doc.autoname.toLowerCase() === 'prompt') {
frm.set_value('naming_rule', 'Set by user');
} else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
frm.set_value('naming_rule', 'Autoincrement');
} else if (frm.doc.autoname.startsWith('field:')) {
frm.set_value('naming_rule', 'By fieldname');
} else if (frm.doc.autoname.startsWith('naming_series:')) {

View file

@ -208,7 +208,7 @@
"label": "Naming"
},
{
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name",
@ -216,6 +216,7 @@
"oldfieldtype": "Data"
},
{
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "name_case",
"fieldtype": "Select",
"label": "Name Case",
@ -282,6 +283,7 @@
},
{
"default": "1",
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@ -565,7 +567,7 @@
"fieldtype": "Select",
"label": "Naming Rule",
"length": 40,
"options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
},
{
"fieldname": "migration_hash",
@ -593,6 +595,7 @@
],
"icon": "fa fa-bolt",
"idx": 6,
"index_web_pages_for_search": 1,
"links": [
{
"group": "Views",
@ -670,10 +673,11 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2022-01-07 16:07:06.196534",
"modified": "2022-02-15 21:47:16.467217",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -703,5 +707,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -60,6 +60,7 @@ class DocType(Document):
self.check_developer_mode()
self.validate_autoname()
self.validate_name()
self.set_defaults_for_single_and_table()
@ -714,6 +715,19 @@ class DocType(Document):
self.name)
return max_idx and max_idx[0][0] or 0
def validate_autoname(self):
if not self.is_new():
doc_before_save = self.get_doc_before_save()
if doc_before_save:
if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") \
or (self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"):
frappe.throw(_("Cannot change to/from Autoincrement naming rule"))
else:
if self.autoname == "autoincrement":
self.allow_rename = 0
frappe.local.autoincremented_doctypes.add(self.name)
def validate_name(self, name=None):
if not name:
name = self.name

View file

@ -505,7 +505,23 @@ class TestDocType(unittest.TestCase):
dt.delete()
def new_doctype(name, unique=0, depends_on='', fields=None):
def test_autoincremented_doctype_transition(self):
frappe.delete_doc("testy_autoinc_dt")
dt = new_doctype("testy_autoinc_dt", autoincremented=True).insert(ignore_permissions=True)
dt.autoname = "hash"
try:
dt.save(ignore_permissions=True)
except frappe.ValidationError as e:
self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule")
else:
self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule")
finally:
# cleanup
dt.delete(ignore_permissions=True)
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
@ -521,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None):
"role": "System Manager",
"read": 1,
}],
"name": name
"name": name,
"autoname": "autoincrement" if autoincremented else ""
})
if fields:

View file

@ -112,7 +112,10 @@ class TestServerScript(unittest.TestCase):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
def test_permission_query(self):
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
if frappe.conf.db_type == "mariadb":
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
else:
self.assertTrue('where (1 = \'1\')' in frappe.db.get_list('ToDo', run=False))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
def test_attribute_error(self):

View file

@ -142,8 +142,6 @@ class Database(object):
self.log_query(query, values, debug, explain)
if values!=():
if isinstance(values, dict):
values = dict(values)
# MySQL-python==1.2.5 hack!
if not isinstance(values, (dict, tuple, list)):

View file

@ -1,12 +1,16 @@
import frappe
from frappe import _
from frappe.database.schema import DBTable
from frappe.database.sequence import create_sequence
from frappe.model import log_types
class MariaDBTable(DBTable):
def create(self):
additional_definitions = ""
engine = self.meta.get("engine") or "InnoDB"
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"
# columns
column_defs = self.get_column_definitions()
@ -29,9 +33,27 @@ class MariaDBTable(DBTable):
)
) + ',\n'
# creating sequence(s)
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
or self.doctype in log_types:
# NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
# it drops the cache and uses the next non cached value in setval func and
# puts that in the backup file, which will start the counter
# from that value when inserting any new record in the doctype.
# By default the cache is 1000 which will mess up the sequence when
# using the system after a restore.
# issue link: https://jira.mariadb.org/browse/MDEV-21786
create_sequence(self.doctype, check_not_exists=True, cache=50)
# 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-21786
name_column = "name bigint primary key"
# create table
query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
{name_column},
creation datetime(6),
modified datetime(6),
modified_by varchar({varchar_len}),

View file

@ -99,8 +99,13 @@ class PostgresDatabase(Database):
return db_size[0].get('database_size')
# pylint: disable=W0221
def sql(self, query, *args, **kwargs):
return super(PostgresDatabase, self).sql(modify_query(query), *args, **kwargs)
def sql(self, query, values=(), *args, **kwargs):
return super(PostgresDatabase, self).sql(
modify_query(query),
modify_values(values),
*args,
**kwargs
)
def get_tables(self, cached=True):
return [d[0] for d in self.sql("""select table_name
@ -333,10 +338,45 @@ def modify_query(query):
if re.search('from tab', query, flags=re.IGNORECASE):
query = re.sub(r'from tab([\w-]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
# only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers),
# drop .0 from decimals and add quotes around them
#
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
# >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
# "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023
query = re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
return query
def modify_values(values):
def stringify_value(value):
if isinstance(value, int):
value = str(value)
elif isinstance(value, float):
truncated_float = int(value)
if value == truncated_float:
value = str(truncated_float)
return value
if not values:
return values
if isinstance(values, dict):
for k, v in values.items():
values[k] = stringify_value(v)
elif isinstance(values, (tuple, list)):
new_values = []
for val in values:
new_values.append(stringify_value(val))
values = new_values
else:
values = stringify_value(values)
return values
def replace_locate_with_strpos(query):
# strpos is the locate equivalent in postgres
if re.search(r'locate\(', query, flags=re.IGNORECASE):
query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
query = re.sub(r'locate\(([^,]+),([^)]+)(\)?)\)', r'strpos(\2\3, \1)', query, flags=re.IGNORECASE)
return query

View file

@ -2,10 +2,14 @@ import frappe
from frappe import _
from frappe.utils import cint, flt
from frappe.database.schema import DBTable, get_definition
from frappe.database.sequence import create_sequence
from frappe.model import log_types
class PostgresTable(DBTable):
def create(self):
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"
additional_definitions = ""
# columns
@ -26,9 +30,21 @@ class PostgresTable(DBTable):
)
)
# creating sequence(s)
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
or self.doctype in log_types:
# The sequence cache is per connection.
# Since we're opening and closing connections for every transaction this results in skipping the cache
# to the next non-cached value hence not using cache in postgres.
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
create_sequence(self.doctype, check_not_exists=True)
name_column = "name bigint primary key"
# TODO: set docstatus length
# create table
frappe.db.sql(f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
{name_column},
creation timestamp(6),
modified timestamp(6),
modified_by varchar({varchar_len}),

View file

@ -0,0 +1,76 @@
from frappe import db, scrub
def create_sequence(
doctype_name: str,
*,
slug: str = "_id_seq",
check_not_exists: bool = False,
cycle: bool = False,
cache: int = 0,
start_value: int = 0,
increment_by: int = 0,
min_value: int = 0,
max_value: int = 0
) -> str:
query = "create sequence"
sequence_name = scrub(doctype_name + slug)
if check_not_exists:
query += " if not exists"
query += f" {sequence_name}"
if cache:
query += f" cache {cache}"
else:
# in postgres, the default is cache 1
if db.db_type == "mariadb":
query += " nocache"
if start_value:
# default is 1
query += f" start with {start_value}"
if increment_by:
# default is 1
query += f" increment by {increment_by}"
if min_value:
# default is 1
query += f" min value {min_value}"
if max_value:
query += f" max value {max_value}"
if not cycle:
if db.db_type == "mariadb":
query += " nocycle"
else:
query += " cycle"
db.sql(query)
return sequence_name
def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int:
if db.db_type == "postgres":
return db.sql(f"select nextval(\'\"{scrub(doctype_name + slug)}\"\')")[0][0]
return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0]
def set_next_val(
doctype_name: str,
next_val: int,
*,
slug: str = "_id_seq",
is_val_used :bool = False
) -> None:
is_val_used = 0 if not is_val_used else 1
if db.db_type == "postgres":
db.sql(f"SELECT SETVAL(\'\"{scrub(doctype_name + slug)}\"\', {next_val}, {is_val_used})")
else:
db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})")

View file

@ -10,6 +10,7 @@ import frappe.desk.form.meta
from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe.utils.data import cstr
from frappe import _
from frappe import _dict
from urllib.parse import quote
@ -356,7 +357,7 @@ def get_document_email(doctype, name):
return None
email = email.split("@")
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(cstr(name)), email[1])
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")

View file

@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt):
def relevance_sorter(key, query, as_dict):
value = _(key.name if as_dict else key[0])
return (
value.lower().startswith(query.lower()) is not True,
cstr(value).lower().startswith(query.lower()) is not True,
value
)

View file

@ -51,7 +51,7 @@ class TestNewsletterMixin:
"reference_name": newsletter,
})
frappe.delete_doc("Newsletter", newsletter)
frappe.db.delete("Newsletter Email Group", newsletter)
frappe.db.delete("Newsletter Email Group", {"parent": newsletter})
newsletters.remove(newsletter)
def setup_email_group(self):

View file

@ -203,12 +203,17 @@ def get_unread_update_logs(consumer_name, dt, dn):
SELECT
update_log.name
FROM `tabEvent Update Log` update_log
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = %(log_name)s
WHERE
consumer.consumer = %(consumer)s
AND update_log.ref_doctype = %(dt)s
AND update_log.docname = %(dn)s
""", {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)]
""", {
"consumer": consumer_name,
"dt": dt,
"dn": dn,
"log_name": "update_log.name" if frappe.conf.db_type == "mariadb" else "CAST(update_log.name AS VARCHAR)"
}, as_dict=0)]
logs = frappe.get_all(
'Event Update Log',

View file

@ -7,6 +7,7 @@ import json
import requests
import frappe
from frappe.utils.data import cstr
class AuthError(Exception):
@ -122,7 +123,7 @@ class FrappeClient(object):
'''Update a remote document
:param doc: dict or Document object to be updated remotely. `name` is mandatory for this'''
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name")
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name"))
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
return frappe._dict(self.post_process(res))
@ -207,7 +208,7 @@ class FrappeClient(object):
if fields:
params["fields"] = json.dumps(fields)
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + cstr(name),
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)

View file

@ -475,7 +475,7 @@ class BaseDocument(object):
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
# don't update name, as case might've been changed
name = d['name']
name = cstr(d['name'])
del d['name']
columns = list(d)

View file

@ -164,7 +164,8 @@ class DatabaseQuery(object):
# left join parent, child tables
for child in self.tables[1:]:
args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
parent_name = self.cast_autoincremented_name(f"{self.tables[0]}.name")
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"
if self.grouped_or_conditions:
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
@ -318,21 +319,63 @@ class DatabaseQuery(object):
]
# add tables from fields
if self.fields:
for field in self.fields:
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
for i, field in enumerate(self.fields):
if not ("tab" in field and "." in field):
continue
# add cast in locate/strpos
func_found = False
for func in sql_functions:
if func in field.lower():
self.fields[i] = self.cast_autoincremented_name(field, func)
func_found = True
break
if func_found:
continue
table_name = field.split('.')[0]
if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
if table_name.lower().startswith('ifnull('):
table_name = table_name[7:]
if not table_name[0]=='`':
table_name = f"`{table_name}`"
if table_name not in self.tables:
self.append_table(table_name)
def cast_autoincremented_name(self, column: str, sql_function: str = "",) -> str:
if frappe.db.db_type == "postgres":
if "name" in column.lower():
if "cast(" not in column.lower() or "::" not in column:
if not sql_function:
return f"cast({column} as varchar)"
elif sql_function == "locate(":
return re.sub(
r'locate\(([^,]+),([^)]+)\)',
r'locate(\1, cast(\2 as varchar))',
column,
flags=re.IGNORECASE
)
elif sql_function == "strpos(":
return re.sub(
r'strpos\(([^,]+),([^)]+)\)',
r'strpos(cast(\1 as varchar), \2)',
column,
flags=re.IGNORECASE
)
elif sql_function == "ifnull(":
return re.sub(
r"ifnull\(([^,]+)",
r"ifnull(cast(\1 as varchar)",
column,
flags=re.IGNORECASE
)
return column
def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
@ -423,6 +466,8 @@ class DatabaseQuery(object):
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
"""
# TODO: refactor
from frappe.boot import get_additional_filters_from_hooks
additional_filters_config = get_additional_filters_from_hooks()
f = get_filter(self.doctype, f, additional_filters_config)
@ -432,15 +477,16 @@ class DatabaseQuery(object):
self.append_table(tname)
if 'ifnull(' in f.fieldname:
column_name = f.fieldname
column_name = self.cast_autoincremented_name(f.fieldname, "ifnull(")
else:
column_name = f"{tname}.{f.fieldname}"
can_be_null = True
column_name = self.cast_autoincremented_name(f"{tname}.{f.fieldname}")
if f.operator.lower() in additional_filters_config:
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
meta = frappe.get_meta(f.doctype)
can_be_null = True
# prepare in condition
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
values = f.value or ''
@ -449,12 +495,8 @@ class DatabaseQuery(object):
# if not isinstance(values, (list, tuple)):
# values = values.split(",")
ref_doctype = f.doctype
if frappe.get_meta(f.doctype).get_field(f.fieldname) is not None :
ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options
result=[]
field = meta.get_field(f.fieldname)
ref_doctype = field.options if field else f.doctype
lft, rgt = '', ''
if f.value:
@ -474,29 +516,30 @@ class DatabaseQuery(object):
}, order_by='`lft` DESC')
fallback = "''"
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
value = [frappe.db.escape((cstr(v.name) or '').strip(), percent=False) for v in result]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
# which can be directly used with IN operator to query.
f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in'
elif f.operator.lower() in ('in', 'not in'):
values = f.value or ''
if isinstance(values, str):
values = values.split(",")
fallback = "''"
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
value = [frappe.db.escape((cstr(v) or '').strip(), percent=False) for v in values]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
else:
df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname})
df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
@ -513,7 +556,8 @@ class DatabaseQuery(object):
fallback = "'0001-01-01 00:00:00'"
elif f.operator.lower() in ('between') and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
(f.fieldname in ('creation', 'modified') or
(df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
value = get_between_date_filter(f.value, df)
fallback = "'0001-01-01 00:00:00'"
@ -528,7 +572,7 @@ class DatabaseQuery(object):
fallback = "''"
can_be_null = True
if 'ifnull' not in column_name:
if 'ifnull' not in column_name.lower():
column_name = f'ifnull({column_name}, {fallback})'
elif df and df.fieldtype=="Date":
@ -570,7 +614,7 @@ class DatabaseQuery(object):
value = f"{tname}.{quote}{f.value.name}{quote}"
# escape value
elif isinstance(value, str) and not f.operator.lower() == 'between':
elif isinstance(value, str) and f.operator.lower() != 'between':
value = f"{frappe.db.escape(value, percent=False)}"
if (

View file

@ -158,7 +158,7 @@ def update_naming_series(doc):
and getattr(doc, "naming_series", None):
revert_series_if_last(doc.naming_series, doc.name, doc)
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"):
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"):
revert_series_if_last(doc.meta.autoname, doc.name, doc)
def delete_from_table(doctype, name, ignore_doctypes, doc):

View file

@ -1,14 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from typing import Optional
from typing import Optional, TYPE_CHECKING
import frappe
from frappe import _
from frappe.database.sequence import get_next_val
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
from frappe.query_builder import DocType
if TYPE_CHECKING:
from frappe.model.meta import Meta
def set_new_name(doc):
"""
@ -24,7 +28,8 @@ def set_new_name(doc):
doc.run_method("before_naming")
autoname = frappe.get_meta(doc.doctype).autoname or ""
meta = frappe.get_meta(doc.doctype)
autoname = meta.autoname or ""
if autoname.lower() != "prompt" and not frappe.flags.in_import:
doc.name = None
@ -36,6 +41,10 @@ def set_new_name(doc):
elif getattr(doc.meta, "issingle", False):
doc.name = doc.doctype
elif is_autoincremented(doc.doctype, meta):
doc.name = get_next_val(doc.doctype)
return
elif getattr(doc.meta, "istable", False):
doc.name = make_autoname("hash", doc.doctype)
@ -67,6 +76,28 @@ def set_new_name(doc):
frappe.get_meta(doc.doctype).get_field("name_case")
)
def is_autoincremented(doctype: str, meta: "Meta" = None):
if doctype in frappe.local.autoincremented_doctypes:
return True
elif doctype in log_types:
if frappe.db.sql(
f"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tab{doctype}'"""
)[0][0] == "bigint":
frappe.local.autoincremented_doctypes.add(doctype)
return True
else:
if not meta:
meta = frappe.get_meta(doctype)
if meta.autoname == "autoincrement":
frappe.local.autoincremented_doctypes.add(doctype)
return True
return False
def set_name_from_naming_options(autoname, doc):
"""
Get a name based on the autoname field option

View file

@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm {
// on main doc
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) {
// set input
if(doc.name===me.docname) {
if (cstr(doc.name) === me.docname) {
me.dirty();
let field = me.fields_dict[fieldname];
@ -1215,7 +1215,7 @@ frappe.ui.form.Form = class FrappeForm {
}
is_dirty() {
return !!this.doc.__unsaved;
return this.doc.__unsaved;
}
is_new() {

View file

@ -915,7 +915,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return this.settings.get_form_link(doc);
}
const docname = doc.name.match(/[%'"#\s]/)
const docname = cstr(doc.name).match(/[%'"#\s]/)
? encodeURIComponent(doc.name)
: doc.name;

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import frappe
from frappe.utils.data import cstr
import os
import redis
@ -118,7 +119,7 @@ def get_user_info():
}
def get_doc_room(doctype, docname):
return ''.join([frappe.local.site, ':doc:', doctype, '/', docname])
return ''.join([frappe.local.site, ':doc:', doctype, '/', cstr(docname)])
def get_user_room(user):
return ''.join([frappe.local.site, ':user:', user])

View file

@ -562,3 +562,50 @@ class TestDDLCommandsPost(unittest.TestCase):
""",
)
self.assertEquals(len(indexs_in_table), 1)
@run_only_if(db_type_is.POSTGRES)
def test_modify_query(self):
from frappe.database.postgres.database import modify_query
query = "select * from `tabtree b` where lft > 13 and rgt <= 16 and name =1.0 and parent = 4134qrsdc and isgroup = 1.00045"
self.assertEqual(
"select * from \"tabtree b\" where lft > \'13\' and rgt <= '16' and name = '1' and parent = 4134qrsdc and isgroup = 1.00045",
modify_query(query)
)
query = "select locate(\".io\", \"frappe.io\"), locate(\"3\", cast(3 as varchar)), locate(\"3\", 3::varchar)"
self.assertEqual(
"select strpos( \"frappe.io\", \".io\"), strpos( cast(3 as varchar), \"3\"), strpos( 3::varchar, \"3\")",
modify_query(query)
)
@run_only_if(db_type_is.POSTGRES)
def test_modify_values(self):
from frappe.database.postgres.database import modify_values
self.assertEqual(
{"abcd": "23", "efgh": "23", "ijkl": 23.0345, "mnop": "wow"},
modify_values({"abcd": 23, "efgh": 23.0, "ijkl": 23.0345, "mnop": "wow"})
)
self.assertEqual(
["23", "23", 23.00004345, "wow"],
modify_values((23, 23.0, 23.00004345, "wow"))
)
def test_sequence_table_creation(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
dt = new_doctype("autoinc_dt_seq_test", autoincremented=True).insert(ignore_permissions=True)
if frappe.db.db_type == "postgres":
self.assertTrue(
frappe.db.sql("""select sequence_name FROM information_schema.sequences
where sequence_name ilike 'autoinc_dt_seq_test%'""")[0][0]
)
else:
self.assertTrue(
frappe.db.sql("""select data_type FROM information_schema.tables
where table_type = 'SEQUENCE' and table_name like 'autoinc_dt_seq_test%'""")[0][0]
)
dt.delete(ignore_permissions=True)

View file

@ -494,6 +494,27 @@ class TestReportview(unittest.TestCase):
response = execute_cmd("frappe.desk.reportview.get")
self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column", 'columns'])
def test_cast_autoincremented_name(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
dt = new_doctype("autoinc_dt_test", autoincremented=True).insert(ignore_permissions=True)
query = DatabaseQuery("autoinc_dt_test").execute(
fields=["locate('1', `tabautoinc_dt_test`.`name`)", "`tabautoinc_dt_test`.`name`"],
filters={"name": 1},
run=False
)
if frappe.db.db_type == "postgres":
self.assertTrue("strpos( cast( \"tabautoinc_dt_test\".\"name\" as varchar), \'1\')" in query)
self.assertTrue("where cast(\"tabautoinc_dt_test\".name as varchar) = \'1\'" in query)
else:
self.assertTrue("locate(\'1\', `tabautoinc_dt_test`.`name`)" in query)
self.assertTrue("where `tabautoinc_dt_test`.name = 1" in query)
dt.delete(ignore_permissions=True)
def add_child_table_to_blog_post():
child_table = frappe.get_doc({
'doctype': 'DocType',

View file

@ -245,6 +245,17 @@ class TestNaming(unittest.TestCase):
})
self.assertRaises(frappe.ValidationError, tag.insert)
def test_autoincremented_naming(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
doctype = "autoinc_doctype" + frappe.generate_hash(length=5)
dt = new_doctype(doctype, autoincremented=True).insert(ignore_permissions=True)
for i in range(1, 20):
self.assertEqual(frappe.new_doc(doctype).save(ignore_permissions=True).name, i)
dt.delete(ignore_permissions=True)
def make_invalid_todo():
frappe.get_doc({

View file

@ -1494,7 +1494,7 @@ def expand_relative_urls(html):
return html
def quoted(url):
return cstr(quote(encode(url), safe=b"~@#$&()*!+=:;,.?/'"))
return cstr(quote(encode(cstr(url)), safe=b"~@#$&()*!+=:;,.?/'"))
def quote_urls(html):
def _quote_url(match):

View file

@ -1,14 +1,15 @@
import json
from difflib import unified_diff
from typing import List
from typing import List, Union
import frappe
from frappe.utils import pretty_date
from frappe.utils.data import cstr
@frappe.whitelist()
def get_version_diff(
from_version: str, to_version: str, fieldname: str = "script"
from_version: Union[int, str], to_version: Union[int, str], fieldname: str = "script"
) -> List[str]:
before, before_timestamp = _get_value_from_version(from_version, fieldname)
@ -23,15 +24,15 @@ def get_version_diff(
diff = unified_diff(
before,
after,
fromfile=from_version,
tofile=to_version,
fromfile=cstr(from_version),
tofile=cstr(to_version),
fromfiledate=before_timestamp,
tofiledate=after_timestamp,
)
return list(diff)
def _get_value_from_version(version_name: str, fieldname: str):
def _get_value_from_version(version_name: Union[int, str], fieldname: str):
version = frappe.get_list(
"Version", fields=["data", "modified"], filters={"name": version_name}
)

View file

@ -9,6 +9,8 @@ import os
from frappe.utils import cint, strip_html_tags
from frappe.utils.html_utils import unescape_html
from frappe.model.base_document import get_controller
from frappe.utils.data import cstr
def setup_global_search_table():
"""
@ -251,7 +253,7 @@ def update_global_search(doc):
if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view:
published = 1 if doc.is_website_published() else 0
title = (doc.get_title() or '')[:int(frappe.db.VARCHAR_LEN)]
title = (cstr(doc.get_title()) or '')[:int(frappe.db.VARCHAR_LEN)]
route = doc.get('route') if doc else ''
value = dict(