Browse Source

fix: Postgres Compatibility

* Handle inconsistencies in type handling in DatabaseQuery & Database
  APIs
* Update incompatible queries with frappe.qb notation
* Fixed use cases discovered by failing ERPNext CI tests

fix: db independent syntax for user_type

fix: handle postgres datetime values

feat: add ability to auto commit on db inserts

feat: add ability to escape underscore in postgres

fix: handle missing data in test runner bootstrapping

fix: db independent syntax for queries

fix: refactor to use qb

fix: update cache for language

fix: use pluck in email_queue

Co-authored-by: gavin <gavin18d@gmail.com>

fix: don't auto insert on tests for make_property_setter

fix: remove auto_commit in custom_field insertion

fix: remove auto_commit functionality

fix: review comments

fix: revert link validation

fix: style suggestion for readability

Co-authored-by: gavin <gavin18d@gmail.com>

fix: revert .lower() in link validation

fix: add rollback for setup_wizard

Revert "fix: add rollback for setup_wizard"

This reverts commit 83b3b0913d.

Revert "feat: add ability to escape underscore in postgres"

This reverts commit 8ed9c2aa33.

fix: more concise representation of order fields

Co-authored-by: gavin <gavin18d@gmail.com>
version-14
Conor 3 years ago
committed by Gavin D'souza
parent
commit
40ba1ac9ba
16 changed files with 77 additions and 52 deletions
  1. +9
    -3
      frappe/core/doctype/user_type/user_type.py
  2. +7
    -1
      frappe/database/postgres/database.py
  3. +2
    -1
      frappe/desk/doctype/bulk_update/bulk_update.py
  4. +3
    -2
      frappe/desk/doctype/dashboard/dashboard.py
  5. +2
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  6. +3
    -2
      frappe/desk/treeview.py
  7. +14
    -22
      frappe/email/doctype/email_queue/email_queue.py
  8. +3
    -1
      frappe/model/base_document.py
  9. +6
    -7
      frappe/model/db_query.py
  10. +4
    -1
      frappe/test_runner.py
  11. +1
    -0
      frappe/tests/test_website.py
  12. +3
    -0
      frappe/utils/data.py
  13. +7
    -3
      frappe/utils/make_random.py
  14. +8
    -6
      frappe/utils/nestedset.py
  15. +1
    -1
      frappe/utils/response.py
  16. +4
    -1
      frappe/website/doctype/blog_post/test_blog_post.py

+ 9
- 3
frappe/core/doctype/user_type/user_type.py View File

@@ -36,13 +36,19 @@ class UserType(Document):
if not self.user_doctypes: if not self.user_doctypes:
return return


modules = frappe.get_all('DocType', fields=['distinct module as module'],
filters={'name': ('in', [d.document_type for d in self.user_doctypes])}, group_by='module')
DocType = frappe.qb.DocType("DocType")

document_types = [d.document_type for d in self.user_doctypes] or ['']
modules = (frappe.qb.from_(DocType)
.select(DocType.module)
.where(DocType.name.isin(document_types))
.groupby(DocType.module)
.distinct()).run()


self.set('user_type_modules', []) self.set('user_type_modules', [])
for row in modules: for row in modules:
self.append('user_type_modules', { self.append('user_type_modules', {
'module': row.module
'module': row[0]
}) })


def validate_document_type_limit(self): def validate_document_type_limit(self):


+ 7
- 1
frappe/database/postgres/database.py View File

@@ -74,9 +74,15 @@ class PostgresDatabase(Database):
return conn return conn


def escape(self, s, percent=True): def escape(self, s, percent=True):
"""Excape quotes and percent in given string."""
"""Escape quotes and percent in given string."""
if isinstance(s, bytes): if isinstance(s, bytes):
s = s.decode('utf-8') s = s.decode('utf-8')
# MariaDB's driver treats None as an empty string
# So Postgres should do the same

if s is None:
s = ''


if percent: if percent:
s = s.replace("%", "%%") s = s.replace("%", "%%")


+ 2
- 1
frappe/desk/doctype/bulk_update/bulk_update.py View File

@@ -7,6 +7,7 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import cint from frappe.utils import cint



class BulkUpdate(Document): class BulkUpdate(Document):
pass pass


@@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500):
frappe.throw(_('; not allowed in condition')) frappe.throw(_('; not allowed in condition'))


docnames = frappe.db.sql_list( docnames = frappe.db.sql_list(
'''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit)
'''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit)
) )
data = {} data = {}
data[field] = value data[field] = value


+ 3
- 2
frappe/desk/doctype/dashboard/dashboard.py View File

@@ -13,8 +13,9 @@ class Dashboard(Document):
def on_update(self): def on_update(self):
if self.is_default: if self.is_default:
# make all other dashboards non-default # make all other dashboards non-default
frappe.db.sql('''update
`tabDashboard` set is_default = 0 where name != %s''', self.name)
DashBoard = frappe.qb.DocType("Dashboard")
frappe.qb.update(DashBoard).set(DashBoard.is_default, 0).where(DashBoard.name != self.name).run()


if frappe.conf.developer_mode and self.is_standard: if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)


+ 2
- 1
frappe/desk/page/setup_wizard/setup_wizard.py View File

@@ -151,7 +151,7 @@ def update_system_settings(args):
system_settings = frappe.get_doc("System Settings", "System Settings") system_settings = frappe.get_doc("System Settings", "System Settings")
system_settings.update({ system_settings.update({
"country": args.get("country"), "country": args.get("country"),
"language": get_language_code(args.get("language")),
"language": get_language_code(args.get("language")) or 'en',
"time_zone": args.get("timezone"), "time_zone": args.get("timezone"),
"float_precision": 3, "float_precision": 3,
'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), 'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"),
@@ -417,6 +417,7 @@ def make_records(records, debug=False):
raise raise


except Exception as e: except Exception as e:
frappe.db.rollback()
exception = record.get('__exception') exception = record.get('__exception')
if exception: if exception:
config = _dict(exception) config = _dict(exception)


+ 3
- 2
frappe/desk/treeview.py View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _



@frappe.whitelist() @frappe.whitelist()
def get_all_nodes(doctype, label, parent, tree_method, **filters): def get_all_nodes(doctype, label, parent, tree_method, **filters):
'''Recursively gets all data from tree nodes''' '''Recursively gets all data from tree nodes'''
@@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters):


def _get_children(doctype, parent='', ignore_permissions=False): def _get_children(doctype, parent='', ignore_permissions=False):
parent_field = 'parent_' + doctype.lower().replace(' ', '_') parent_field = 'parent_' + doctype.lower().replace(' ', '_')
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
['docstatus', '<' ,'2']]
filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent],
['docstatus', '<' ,2]]


meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)




+ 14
- 22
frappe/email/doctype/email_queue/email_queue.py View File

@@ -475,28 +475,20 @@ class QueueBuilder:
if self._unsubscribed_user_emails is not None: if self._unsubscribed_user_emails is not None:
return self._unsubscribed_user_emails return self._unsubscribed_user_emails


all_ids = tuple(set(self.recipients + self.cc))

unsubscribed = frappe.db.sql_list('''
SELECT
distinct email
from
`tabEmail Unsubscribe`
where
email in %(all_ids)s
and (
(
reference_doctype = %(reference_doctype)s
and reference_name = %(reference_name)s
)
or global_unsubscribe = 1
)
''', {
'all_ids': all_ids,
'reference_doctype': self.reference_doctype,
'reference_name': self.reference_name,
})

all_ids = list(set(self.recipients + self.cc))

EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe")

unsubscribed = (frappe.qb.from_(EmailUnsubscribe)
.select(EmailUnsubscribe.email)
.where(EmailUnsubscribe.email.isin(all_ids) &
(
(
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name)
) | EmailUnsubscribe.global_unsubscribe == 1
)
).distinct()
).run(pluck=True)
self._unsubscribed_user_emails = unsubscribed or [] self._unsubscribed_user_emails = unsubscribed or []
return self._unsubscribed_user_emails return self._unsubscribed_user_emails




+ 3
- 1
frappe/model/base_document.py View File

@@ -771,7 +771,9 @@ class BaseDocument(object):


else: else:
self_value = self.get_value(key) self_value = self.get_value(key)

# Postgres stores values as `datetime.time`, MariaDB as `timedelta`
if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time):
db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond)
if self_value != db_value: if self_value != db_value:
frappe.throw(_("Not allowed to change {0} after submission").format(df.label), frappe.throw(_("Not allowed to change {0} after submission").format(df.label),
frappe.UpdateAfterSubmitError) frappe.UpdateAfterSubmitError)


+ 6
- 7
frappe/model/db_query.py View File

@@ -127,19 +127,18 @@ class DatabaseQuery(object):
if self.distinct: if self.distinct:
args.fields = 'distinct ' + args.fields args.fields = 'distinct ' + args.fields
args.order_by = '' # TODO: recheck for alternative args.order_by = '' # TODO: recheck for alternative

# Postgres requires any field that appears in the select clause to also
# appear in the order by and group by clause
if args.order_by and args.group_by: if args.order_by and args.group_by:
order_field = args.order_by order_field = args.order_by


for r in (" order by ", " asc", " ASC", " desc", " DESC"): for r in (" order by ", " asc", " ASC", " desc", " DESC"):
order_field = order_field.replace(r, "") order_field = order_field.replace(r, "")


if not order_field in args.fields:
order_fieldm = order_field.replace("`", "")
if "." in order_fieldm:
args.fields += ", MAX(" + order_fieldm.split(".")[1] + ") as `" + order_fieldm + "`"
else:
args.fields += ", MAX(" + order_fieldm + ") as `" + order_fieldm + "`"
if order_field not in args.fields:
order_fieldm = order_field.replace("`", '"')
args.fields += f", MAX({order_fieldm}) as `{order_fieldm}`"
args.order_by = args.order_by.replace(order_field, "`" + order_fieldm + "`") args.order_by = args.order_by.replace(order_field, "`" + order_fieldm + "`")


query = """select %(fields)s query = """select %(fields)s


+ 4
- 1
frappe/test_runner.py View File

@@ -335,7 +335,10 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) frappe.local.test_objects[doctype] += test_module._make_test_records(verbose)


elif hasattr(test_module, "test_records"): elif hasattr(test_module, "test_records"):
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
if doctype in frappe.local.test_objects:
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
else:
frappe.local.test_objects[doctype] = make_test_objects(doctype, test_module.test_records, verbose, force)


else: else:
test_records = frappe.get_test_records(doctype) test_records = frappe.get_test_records(doctype)


+ 1
- 0
frappe/tests/test_website.py View File

@@ -197,6 +197,7 @@ class TestWebsite(unittest.TestCase):
frappe.cache().delete_key('app_hooks') frappe.cache().delete_key('app_hooks')


def test_printview_page(self): def test_printview_page(self):
frappe.db.value_cache[('DocType', 'Language', 'name')] = (('Language',),)
content = get_response_content('/Language/ru') content = get_response_content('/Language/ru')
self.assertIn('<div class="print-format">', content) self.assertIn('<div class="print-format">', content)
self.assertIn('<div>Language</div>', content) self.assertIn('<div>Language</div>', content)


+ 3
- 0
frappe/utils/data.py View File

@@ -98,6 +98,9 @@ def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]:
def to_timedelta(time_str): def to_timedelta(time_str):
from dateutil import parser from dateutil import parser


if isinstance(time_str, datetime.time):
time_str = str(time_str)

if isinstance(time_str, str): if isinstance(time_str, str):
t = parser.parse(time_str) t = parser.parse(time_str)
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond) return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)


+ 7
- 3
frappe/utils/make_random.py View File

@@ -35,9 +35,13 @@ def get_random(doctype, filters=None, doc=False):
condition = " where " + " and ".join(condition) condition = " where " + " and ".join(condition)
else: else:
condition = "" condition = ""

out = frappe.db.sql("""select name from `tab%s` %s
order by RAND() limit 0,1""" % (doctype, condition))
out = frappe.db.multisql({
'mariadb': """select name from `tab%s` %s
order by RAND() limit 1 offset 0""" % (doctype, condition),
'postgres': """select name from `tab%s` %s
order by RANDOM() limit 1 offset 0""" % (doctype, condition)
})


out = out and out[0][0] or None out = out and out[0][0] or None




+ 8
- 6
frappe/utils/nestedset.py View File

@@ -16,6 +16,9 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import DocType, Order from frappe.query_builder import DocType, Order
from pypika import CustomFunction

Replace = CustomFunction('REPLACE', ['input', 'substr_to_replace', 'substr_to_replace_with'])


class NestedSetRecursionError(frappe.ValidationError): pass class NestedSetRecursionError(frappe.ValidationError): pass
class NestedSetMultipleRootsError(frappe.ValidationError): pass class NestedSetMultipleRootsError(frappe.ValidationError): pass
@@ -88,7 +91,7 @@ def update_move_node(doc, parent_field):


if parent: if parent:
new_parent = frappe.db.sql("""select lft, rgt from `tab{0}` new_parent = frappe.db.sql("""select lft, rgt from `tab{0}`
where name = %s for update""".format(doc.doctype), parent, as_dict=1)[0]
where name = %s for update""".format(doc.doctype), parent, as_dict=1)[0]


validate_loop(doc.doctype, doc.name, new_parent.lft, new_parent.rgt) validate_loop(doc.doctype, doc.name, new_parent.lft, new_parent.rgt)


@@ -107,8 +110,7 @@ def update_move_node(doc, parent_field):


if parent: if parent:
new_parent = frappe.db.sql("""select lft, rgt from `tab%s` new_parent = frappe.db.sql("""select lft, rgt from `tab%s`
where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0]

where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0]


# set parent lft, rgt # set parent lft, rgt
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s frappe.db.sql("""update `tab{0}` set rgt = rgt + %s
@@ -134,6 +136,7 @@ def update_move_node(doc, parent_field):
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s
where lft < 0""".format(doc.doctype), (new_diff, new_diff)) where lft < 0""".format(doc.doctype), (new_diff, new_diff))



@frappe.whitelist() @frappe.whitelist()
def rebuild_tree(doctype, parent_field): def rebuild_tree(doctype, parent_field):
""" """
@@ -148,13 +151,12 @@ def rebuild_tree(doctype, parent_field):
right = 1 right = 1
table = DocType(doctype) table = DocType(doctype)
column = getattr(table, parent_field) column = getattr(table, parent_field)

result = ( result = (
frappe.qb.from_(table) frappe.qb.from_(table)
.where( .where(
(column == "") | (column.isnull()) (column == "") | (column.isnull())
) )
.orderby(table.name, order=Order.asc)
.orderby(Replace(table.name, '_', ''), order=Order.asc)
.select(table.name) .select(table.name)
).run() ).run()


@@ -177,7 +179,7 @@ def rebuild_node(doctype, parent, left, parent_field):
column = getattr(table, parent_field) column = getattr(table, parent_field)


result = ( result = (
frappe.qb.from_(table).where(column == parent).select(table.name)
frappe.qb.from_(table).where(column == parent).select(table.name).orderby(Replace(table.name, '_', ''), order=Order.asc)
).run() ).run()


for r in result: for r in result:


+ 1
- 1
frappe/utils/response.py View File

@@ -125,7 +125,7 @@ def json_handler(obj):
# serialize date # serialize date
import collections.abc import collections.abc


if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)):
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime, datetime.time)):
return str(obj) return str(obj)


elif isinstance(obj, decimal.Decimal): elif isinstance(obj, decimal.Decimal):


+ 4
- 1
frappe/website/doctype/blog_post/test_blog_post.py View File

@@ -58,15 +58,18 @@ class TestBlogPost(unittest.TestCase):
category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0] category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0]
category_page_url = category_page_link["href"] category_page_url = category_page_link["href"]


cached_value = frappe.db.value_cache[('DocType', 'Blog Post', 'name')]
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = (('Blog Post',),)

# Visit the category page (by following the link found in above stage) # Visit the category page (by following the link found in above stage)
set_request(path=category_page_url) set_request(path=category_page_url)
category_page_response = get_response() category_page_response = get_response()
category_page_html = frappe.safe_decode(category_page_response.get_data()) category_page_html = frappe.safe_decode(category_page_response.get_data())

# Category page should contain the blog post title # Category page should contain the blog post title
self.assertIn(blog.title, category_page_html) self.assertIn(blog.title, category_page_html)


# Cleanup # Cleanup
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = cached_value
frappe.delete_doc("Blog Post", blog.name) frappe.delete_doc("Blog Post", blog.name)
frappe.delete_doc("Blog Category", blog.blog_category) frappe.delete_doc("Blog Category", blog.blog_category)




Loading…
Cancel
Save