# Copyright (c) 2022, JINGROW and Contributors
# License: MIT. See LICENSE

import datetime
from math import ceil
from random import choice
from unittest.mock import patch

import jingrow
from jingrow.core.utils import find
from jingrow.custom.pagetype.custom_field.custom_field import create_custom_field
from jingrow.database import savepoint
from jingrow.database.database import get_query_execution_timeout
from jingrow.database.utils import FallBackDateTimeStr
from jingrow.query_builder import Field
from jingrow.query_builder.functions import Concat_ws
from jingrow.tests.test_query_builder import db_type_is, run_only_if
from jingrow.tests.utils import JingrowTestCase, timeout
from jingrow.utils import add_days, now, random_string, set_request
from jingrow.utils.testutils import clear_custom_fields


class TestDB(JingrowTestCase):
	def test_datetime_format(self):
		now_str = now()
		self.assertEqual(jingrow.db.format_datetime(None), FallBackDateTimeStr)
		self.assertEqual(jingrow.db.format_datetime(now_str), now_str)

	@run_only_if(db_type_is.MARIADB)
	def test_get_column_type(self):
		desc_data = jingrow.db.sql("desc `tabUser`", as_dict=1)
		user_name_type = find(desc_data, lambda x: x["Field"] == "name")["Type"]
		self.assertEqual(jingrow.db.get_column_type("User", "name"), user_name_type)

	def test_get_database_size(self):
		self.assertIsInstance(jingrow.db.get_database_size(), (float, int))

	def test_db_statement_execution_timeout(self):
		jingrow.db.set_execution_timeout(2)
		# Setting 0 means no timeout.
		self.addCleanup(jingrow.db.set_execution_timeout, 0)

		try:
			savepoint = "statement_timeout"
			jingrow.db.savepoint(savepoint)
			jingrow.db.multisql(
				{
					"mariadb": "select sleep(10)",
					"postgres": "select pg_sleep(10)",
				}
			)
		except Exception as e:
			self.assertTrue(jingrow.db.is_statement_timeout(e), f"exepcted {e} to be timeout error")
			jingrow.db.rollback(save_point=savepoint)
		else:
			jingrow.db.rollback(save_point=savepoint)
			self.fail("Long running queries not timing out")

	@patch.dict(jingrow.conf, {"http_timeout": 20, "enable_db_statement_timeout": 1})
	def test_db_timeout_computation(self):
		set_request(method="GET", path="/")
		self.assertEqual(get_query_execution_timeout(), 30)
		jingrow.local.request = None
		self.assertEqual(get_query_execution_timeout(), 0)

	def test_get_value(self):
		self.assertEqual(jingrow.db.get_value("User", {"name": ["=", "Administrator"]}), "Administrator")
		self.assertEqual(jingrow.db.get_value("User", {"name": ["like", "Admin%"]}), "Administrator")
		self.assertNotEqual(jingrow.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
		self.assertEqual(jingrow.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
		self.assertEqual(jingrow.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
		self.assertEqual(
			jingrow.db.get_value("User", {}, ["Max(name)"], order_by=None),
			jingrow.db.sql("SELECT Max(name) FROM tabUser")[0][0],
		)
		self.assertEqual(
			jingrow.db.get_value("User", {}, "Min(name)", order_by=None),
			jingrow.db.sql("SELECT Min(name) FROM tabUser")[0][0],
		)
		self.assertIn(
			"for update",
			jingrow.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower(),
		)
		user_pagetype = jingrow.qb.PageType("User")
		self.assertEqual(
			jingrow.qb.from_(user_pagetype).select(user_pagetype.name, user_pagetype.email).run(),
			jingrow.db.get_values(
				user_pagetype,
				filters={},
				fieldname=[user_pagetype.name, user_pagetype.email],
				order_by=None,
			),
		)
		self.assertEqual(
			jingrow.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
			jingrow.db.get_value("User", {"name": [">", "s"]}),
		)

		self.assertEqual(
			jingrow.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
			jingrow.db.get_value("User", {"name": [">=", "t"]}),
		)
		self.assertEqual(
			jingrow.db.get_values(
				"User",
				filters={"name": "Administrator"},
				distinct=True,
				fieldname="email",
			),
			jingrow.qb.from_(user_pagetype)
			.where(user_pagetype.name == "Administrator")
			.select("email")
			.distinct()
			.run(),
		)

		self.assertIn(
			"concat_ws",
			jingrow.db.get_value(
				"User",
				filters={"name": "Administrator"},
				fieldname=Concat_ws(" ", "LastName"),
				run=False,
			).lower(),
		)
		self.assertEqual(
			jingrow.db.sql("select email from tabUser where name='Administrator' order by modified DESC"),
			jingrow.db.get_values("User", filters=[["name", "=", "Administrator"]], fieldname="email"),
		)

		# test multiple orderby's
		delimiter = '"' if jingrow.db.db_type == "postgres" else "`"
		self.assertIn(
			"ORDER BY {deli}creation{deli} DESC,{deli}modified{deli} ASC,{deli}name{deli} DESC".format(
				deli=delimiter
			),
			jingrow.db.get_value("PageType", "PageField", order_by="creation desc, modified asc, name", run=0),
		)

	def test_escape(self):
		jingrow.db.escape("香港濟生堂製藥有限公司 - IT".encode())

	def test_get_single_value(self):
		# setup
		values_dict = {
			"Float": 1.5,
			"Int": 1,
			"Percent": 55.5,
			"Currency": 12.5,
			"Data": "Test",
			"Date": datetime.datetime.now().date(),
			"Datetime": datetime.datetime.now(),
			"Time": datetime.timedelta(hours=9, minutes=45, seconds=10),
		}
		test_inputs = [{"fieldtype": fieldtype, "value": value} for fieldtype, value in values_dict.items()]
		for fieldtype in values_dict:
			create_custom_field(
				"Print Settings",
				{
					"fieldname": f"test_{fieldtype.lower()}",
					"label": f"Test {fieldtype}",
					"fieldtype": fieldtype,
				},
			)

		# test
		for inp in test_inputs:
			fieldname = f"test_{inp['fieldtype'].lower()}"
			jingrow.db.set_single_value("Print Settings", fieldname, inp["value"])
			self.assertEqual(jingrow.db.get_single_value("Print Settings", fieldname), inp["value"])

		# teardown
		clear_custom_fields("Print Settings")

	def test_get_single_value_destructuring(self):
		[[lang, date_format]] = jingrow.db.get_values_from_single(
			["language", "date_format"], None, "System Settings"
		)
		self.assertEqual(lang, jingrow.db.get_single_value("System Settings", "language"))
		self.assertEqual(date_format, jingrow.db.get_single_value("System Settings", "date_format"))

	def test_log_touched_tables(self):
		jingrow.flags.in_migrate = True
		jingrow.flags.touched_tables = set()
		jingrow.db.set_single_value("System Settings", "backup_limit", 5)
		self.assertIn("tabSingles", jingrow.flags.touched_tables)

		jingrow.flags.touched_tables = set()
		todo = jingrow.get_pg({"pagetype": "ToDo", "description": "Random Description"})
		todo.save()
		self.assertIn("tabToDo", jingrow.flags.touched_tables)

		jingrow.flags.touched_tables = set()
		todo.description = "Another Description"
		todo.save()
		self.assertIn("tabToDo", jingrow.flags.touched_tables)

		if jingrow.db.db_type != "postgres":
			jingrow.flags.touched_tables = set()
			jingrow.db.sql("UPDATE tabToDo SET description = 'Updated Description'")
			self.assertNotIn("tabToDo SET", jingrow.flags.touched_tables)
			self.assertIn("tabToDo", jingrow.flags.touched_tables)

		jingrow.flags.touched_tables = set()
		todo.delete()
		self.assertIn("tabToDo", jingrow.flags.touched_tables)

		jingrow.flags.touched_tables = set()
		cf = create_custom_field("ToDo", {"label": "ToDo Custom Field"})
		self.assertIn("tabToDo", jingrow.flags.touched_tables)
		self.assertIn("tabCustom Field", jingrow.flags.touched_tables)
		if cf:
			cf.delete()
		jingrow.db.commit()
		jingrow.flags.in_migrate = False
		jingrow.flags.touched_tables.clear()

	def test_db_keywords_as_fields(self):
		"""Tests if DB keywords work as pagefield names. If they're wrapped with grave accents."""
		# Using random.choices, picked out a list of 40 keywords for testing
		all_keywords = {
			"mariadb": [
				"CHARACTER",
				"DELAYED",
				"LINES",
				"EXISTS",
				"YEAR_MONTH",
				"LOCALTIME",
				"BOTH",
				"MEDIUMINT",
				"LEFT",
				"BINARY",
				"DEFAULT",
				"KILL",
				"WRITE",
				"SQL_SMALL_RESULT",
				"CURRENT_TIME",
				"CROSS",
				"INHERITS",
				"SELECT",
				"TABLE",
				"ALTER",
				"CURRENT_TIMESTAMP",
				"XOR",
				"CASE",
				"ALL",
				"WHERE",
				"INT",
				"TO",
				"SOME",
				"DAY_MINUTE",
				"ERRORS",
				"OPTIMIZE",
				"REPLACE",
				"HIGH_PRIORITY",
				"VARBINARY",
				"HELP",
				"IS",
				"CHAR",
				"DESCRIBE",
				"KEY",
			],
			"postgres": [
				"WORK",
				"LANCOMPILER",
				"REAL",
				"HAVING",
				"REPEATABLE",
				"DATA",
				"USING",
				"BIT",
				"DEALLOCATE",
				"SERIALIZABLE",
				"CURSOR",
				"INHERITS",
				"ARRAY",
				"TRUE",
				"IGNORE",
				"PARAMETER_MODE",
				"ROW",
				"CHECKPOINT",
				"SHOW",
				"BY",
				"SIZE",
				"SCALE",
				"UNENCRYPTED",
				"WITH",
				"AND",
				"CONVERT",
				"FIRST",
				"SCOPE",
				"WRITE",
				"INTERVAL",
				"CHARACTER_SET_SCHEMA",
				"ADD",
				"SCROLL",
				"NULL",
				"WHEN",
				"TRANSACTION_ACTIVE",
				"INT",
				"FORTRAN",
				"STABLE",
			],
		}
		created_pgs = []

		# edit by rushabh: added [:1]
		# don't run every keyword! - if one works, they all do
		fields = all_keywords[jingrow.conf.db_type][:1]
		test_pagetype = "ToDo"

		def add_custom_field(field):
			create_custom_field(
				test_pagetype,
				{
					"fieldname": field.lower(),
					"label": field.title(),
					"fieldtype": "Data",
				},
			)

		# Create custom fields for test_pagetype
		for field in fields:
			add_custom_field(field)

		# Create documents under that pagetype and query them via ORM
		for _ in range(10):
			pagefields = {key.lower(): random_string(10) for key in fields}
			pg = jingrow.get_pg({"pagetype": test_pagetype, "description": random_string(20), **pagefields})
			pg.insert()
			created_pgs.append(pg.name)

		random_field = choice(fields).lower()
		random_pg = choice(created_pgs)
		random_value = random_string(20)

		# Testing read
		self.assertEqual(next(iter(jingrow.get_all("ToDo", fields=[random_field], limit=1)[0])), random_field)
		self.assertEqual(
			next(iter(jingrow.get_all("ToDo", fields=[f"`{random_field}` as total"], limit=1)[0])), "total"
		)

		# Testing read for distinct and sql functions
		self.assertEqual(
			next(
				iter(
					jingrow.get_all(
						"ToDo",
						fields=[f"`{random_field}` as total"],
						distinct=True,
						limit=1,
					)[0]
				)
			),
			"total",
		)
		self.assertEqual(
			next(
				iter(
					jingrow.get_all(
						"ToDo",
						fields=[f"`{random_field}`"],
						distinct=True,
						limit=1,
					)[0]
				)
			),
			random_field,
		)
		self.assertEqual(
			next(iter(jingrow.get_all("ToDo", fields=[f"count(`{random_field}`)"], limit=1)[0])),
			"count" if jingrow.conf.db_type == "postgres" else f"count(`{random_field}`)",
		)

		# Testing update
		jingrow.db.set_value(test_pagetype, random_pg, random_field, random_value)
		self.assertEqual(jingrow.db.get_value(test_pagetype, random_pg, random_field), random_value)

		# Cleanup - delete records and remove custom fields
		for pg in created_pgs:
			jingrow.delete_pg(test_pagetype, pg)
		clear_custom_fields(test_pagetype)

	def test_savepoints(self):
		jingrow.db.rollback()
		save_point = "todonope"

		created_pgs = []
		failed_pgs = []

		for _ in range(5):
			jingrow.db.savepoint(save_point)
			pg_gone = jingrow.get_pg(pagetype="ToDo", description="nope").save()
			failed_pgs.append(pg_gone.name)
			jingrow.db.rollback(save_point=save_point)
			pg_kept = jingrow.get_pg(pagetype="ToDo", description="nope").save()
			created_pgs.append(pg_kept.name)
		jingrow.db.commit()

		for d in failed_pgs:
			self.assertFalse(jingrow.db.exists("ToDo", d))
		for d in created_pgs:
			self.assertTrue(jingrow.db.exists("ToDo", d))

	def test_savepoints_wrapper(self):
		jingrow.db.rollback()

		class SpecificExc(Exception):
			pass

		created_pgs = []
		failed_pgs = []

		for _ in range(5):
			with savepoint(catch=SpecificExc):
				pg_kept = jingrow.get_pg(pagetype="ToDo", description="nope").save()
				created_pgs.append(pg_kept.name)

			with savepoint(catch=SpecificExc):
				pg_gone = jingrow.get_pg(pagetype="ToDo", description="nope").save()
				failed_pgs.append(pg_gone.name)
				raise SpecificExc

		jingrow.db.commit()

		for d in failed_pgs:
			self.assertFalse(jingrow.db.exists("ToDo", d))
		for d in created_pgs:
			self.assertTrue(jingrow.db.exists("ToDo", d))

	def test_transaction_writes_error(self):
		from jingrow.database.database import Database

		jingrow.db.rollback()

		jingrow.db.MAX_WRITES_PER_TRANSACTION = 1
		note = jingrow.get_last_pg("ToDo")
		note.description = "changed"
		with self.assertRaises(jingrow.TooManyWritesError):
			note.save()

		jingrow.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION

	def test_transaction_write_counting(self):
		note = jingrow.get_pg(pagetype="Note", title="transaction counting").insert()

		writes = jingrow.db.transaction_writes
		jingrow.db.set_value("Note", note.name, "content", "abc")
		self.assertEqual(1, jingrow.db.transaction_writes - writes)
		writes = jingrow.db.transaction_writes

		jingrow.db.sql(
			"""
			update `tabNote`
			set content = 'abc'
			where name = %s
			""",
			note.name,
		)
		self.assertEqual(1, jingrow.db.transaction_writes - writes)

	def test_pk_collision_ignoring(self):
		# note has `name` generated from title
		for _ in range(3):
			jingrow.get_pg(pagetype="Note", title="duplicate name").insert(ignore_if_duplicate=True)

		with savepoint():
			self.assertRaises(
				jingrow.DuplicateEntryError, jingrow.get_pg(pagetype="Note", title="duplicate name").insert
			)
			# recover transaction to continue other tests
			raise Exception

	def test_read_only_errors(self):
		jingrow.db.rollback()
		jingrow.db.begin(read_only=True)
		self.addCleanup(jingrow.db.rollback)

		with self.assertRaises(jingrow.InReadOnlyMode):
			jingrow.db.set_value("User", "Administrator", "full_name", "Haxor")

	def test_exists(self):
		dt, dn = "User", "Administrator"
		self.assertEqual(jingrow.db.exists(dt, dn, cache=True), dn)
		self.assertEqual(jingrow.db.exists(dt, dn), dn)
		self.assertEqual(jingrow.db.exists(dt, {"name": ("=", dn)}), dn)

		filters = {"pagetype": dt, "name": ("like", "Admin%")}
		self.assertEqual(jingrow.db.exists(filters), dn)
		self.assertEqual(filters["pagetype"], dt)  # make sure that pagetype was not removed from filters

		self.assertEqual(jingrow.db.exists(dt, [["name", "=", dn]]), dn)

	def test_estimated_count(self):
		self.assertGreater(jingrow.db.estimate_count("PageField"), 100)

	def test_bulk_insert(self):
		current_count = jingrow.db.count("ToDo")
		test_body = f"test_bulk_insert - {random_string(10)}"
		chunk_size = 10

		for number_of_values in (1, 2, 5, 27):
			current_transaction_writes = jingrow.db.transaction_writes

			jingrow.db.bulk_insert(
				"ToDo",
				["name", "description"],
				[[f"ToDo Test Bulk Insert {i}", test_body] for i in range(number_of_values)],
				ignore_duplicates=True,
				chunk_size=chunk_size,
			)

			# check that all records were inserted
			self.assertEqual(number_of_values, jingrow.db.count("ToDo") - current_count)

			# check if inserts were done in chunks
			expected_number_of_writes = ceil(number_of_values / chunk_size)
			self.assertEqual(
				expected_number_of_writes, jingrow.db.transaction_writes - current_transaction_writes
			)

		jingrow.db.delete("ToDo", {"description": test_body})

	def test_bulk_update(self):
		test_body = f"test_bulk_update - {random_string(10)}"

		jingrow.db.bulk_insert(
			"ToDo",
			["name", "description"],
			[[f"ToDo Test Bulk Update {i}", test_body] for i in range(20)],
			ignore_duplicates=True,
		)

		record_names = jingrow.get_all("ToDo", filters={"description": test_body}, pluck="name")

		new_descriptions = {name: f"{test_body} - updated - {random_string(10)}" for name in record_names}

		# update with same fields to update
		jingrow.db.bulk_update(
			"ToDo", {name: {"description": new_descriptions[name]} for name in record_names}
		)

		# check if all records were updated
		updated_records = dict(
			jingrow.get_all(
				"ToDo", filters={"name": ("in", record_names)}, fields=["name", "description"], as_list=True
			)
		)
		self.assertDictEqual(new_descriptions, updated_records)

		# update with different fields to update
		updates = {
			record_names[0]: {"priority": "High", "status": "Closed"},
			record_names[1]: {"status": "Closed"},
		}
		jingrow.db.bulk_update("ToDo", updates)

		priority, status = jingrow.db.get_value("ToDo", record_names[0], ["priority", "status"])

		self.assertEqual(priority, "High")
		self.assertEqual(status, "Closed")

		# further updates with different fields to update
		updates = {record_names[0]: {"status": "Open"}, record_names[1]: {"priority": "Low"}}
		jingrow.db.bulk_update("ToDo", updates)

		priority, status = jingrow.db.get_value("ToDo", record_names[0], ["priority", "status"])
		self.assertEqual(priority, "High")  # should stay the same
		self.assertEqual(status, "Open")

		priority, status = jingrow.db.get_value("ToDo", record_names[1], ["priority", "status"])
		self.assertEqual(priority, "Low")
		self.assertEqual(status, "Closed")  # should stay the same

		# cleanup
		jingrow.db.delete("ToDo", {"name": ("in", record_names)})

	def test_count(self):
		jingrow.db.delete("Note")

		jingrow.get_pg(pagetype="Note", title="note1", content="something").insert()
		jingrow.get_pg(pagetype="Note", title="note2", content="someting else").insert()

		# Count with no filtes
		self.assertEqual((jingrow.db.count("Note")), 2)

		# simple filters
		self.assertEqual((jingrow.db.count("Note", [["title", "=", "note1"]])), 1)

		jingrow.get_pg(pagetype="Note", title="note3", content="something other").insert()

		# List of list filters with tables
		self.assertEqual(
			(
				jingrow.db.count(
					"Note",
					[["Note", "title", "like", "note%"], ["Note", "content", "like", "some%"]],
				)
			),
			3,
		)

		jingrow.db.rollback()

	@run_only_if(db_type_is.POSTGRES)
	def test_modify_query(self):
		from jingrow.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", "jingrow.com"), locate("3", cast(3 as varchar)), locate("3", 3::varchar)'
		self.assertEqual(
			'select strpos( "jingrow.com", ".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 jingrow.database.postgres.database import modify_values

		self.assertEqual(
			{"a": "23", "b": 23.0, "c": 23.0345, "d": "wow", "e": ("1", "2", "3", "abc")},
			modify_values({"a": 23, "b": 23.0, "c": 23.0345, "d": "wow", "e": [1, 2, 3, "abc"]}),
		)
		self.assertEqual(
			["23", 23.0, 23.00004345, "wow", ("1", "2", "3", "abc")],
			modify_values((23, 23.0, 23.00004345, "wow", [1, 2, 3, "abc"])),
		)

	def test_callbacks(self):
		order_of_execution = []

		def f(val):
			nonlocal order_of_execution
			order_of_execution.append(val)

		jingrow.db.before_commit.add(lambda: f(0))
		jingrow.db.before_commit.add(lambda: f(1))

		jingrow.db.after_commit.add(lambda: f(2))
		jingrow.db.after_commit.add(lambda: f(3))

		jingrow.db.before_rollback.add(lambda: f("IGNORED"))
		jingrow.db.before_rollback.add(lambda: f("IGNORED"))

		jingrow.db.commit()

		jingrow.db.after_commit.add(lambda: f("IGNORED"))
		jingrow.db.after_commit.add(lambda: f("IGNORED"))

		jingrow.db.before_rollback.add(lambda: f(4))
		jingrow.db.before_rollback.add(lambda: f(5))
		jingrow.db.after_rollback.add(lambda: f(6))
		jingrow.db.after_rollback.add(lambda: f(7))
		jingrow.db.after_rollback(lambda: f(8))

		jingrow.db.rollback()

		self.assertEqual(order_of_execution, list(range(0, 9)))


@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(JingrowTestCase):
	test_table_name = "TestNotes"

	def setUp(self) -> None:
		jingrow.db.sql_ddl(
			f"""
			CREATE TABLE IF NOT EXISTS `tab{self.test_table_name}` (`id` INT NULL, content TEXT, PRIMARY KEY (`id`));
			"""
		)

	def tearDown(self) -> None:
		jingrow.db.sql(f"DROP TABLE tab{self.test_table_name};")
		self.test_table_name = "TestNotes"

	def test_rename(self) -> None:
		new_table_name = f"{self.test_table_name}_new"
		jingrow.db.rename_table(self.test_table_name, new_table_name)
		check_exists = jingrow.db.sql(
			f"""
			SELECT * FROM INFORMATION_SCHEMA.TABLES
			WHERE TABLE_NAME = N'tab{new_table_name}';
			"""
		)
		self.assertGreater(len(check_exists), 0)
		self.assertIn(f"tab{new_table_name}", check_exists[0])

		# * so this table is deleted after the rename
		self.test_table_name = new_table_name

	def test_describe(self) -> None:
		self.assertSequenceEqual(
			[
				("id", "int(11)", "NO", "PRI", None, ""),
				("content", "text", "YES", "", None, ""),
			],
			jingrow.db.describe(self.test_table_name),
		)

	def test_change_type(self) -> None:
		def get_table_description():
			return jingrow.db.sql(f"DESC `tab{self.test_table_name}`")

		# try changing from int to varchar
		jingrow.db.change_column_type("TestNotes", "id", "varchar(255)")
		self.assertIn("varchar(255)", get_table_description()[0])

		# try changing from varchar to bigint
		jingrow.db.change_column_type("TestNotes", "id", "bigint")
		self.assertIn("bigint(20)", get_table_description()[0])

	def test_add_index(self) -> None:
		index_name = "test_index"
		jingrow.db.add_index(self.test_table_name, ["id", "content(50)"], index_name)
		indexs_in_table = jingrow.db.sql(
			f"""
			SHOW INDEX FROM tab{self.test_table_name}
			WHERE Key_name = '{index_name}';
			"""
		)
		self.assertEqual(len(indexs_in_table), 2)


class TestDBSetValue(JingrowTestCase):
	@classmethod
	def setUpClass(cls):
		super().setUpClass()
		cls.todo1 = jingrow.get_pg(pagetype="ToDo", description="test_set_value 1").insert()
		cls.todo2 = jingrow.get_pg(pagetype="ToDo", description="test_set_value 2").insert()

	def test_update_single_pagetype_field(self):
		value = jingrow.db.get_single_value("System Settings", "deny_multiple_sessions")
		changed_value = not value

		jingrow.db.set_single_value("System Settings", "deny_multiple_sessions", changed_value)
		current_value = jingrow.db.get_single_value("System Settings", "deny_multiple_sessions")
		self.assertEqual(current_value, changed_value)

		changed_value = not current_value
		jingrow.db.set_single_value("System Settings", "deny_multiple_sessions", changed_value)
		current_value = jingrow.db.get_single_value("System Settings", "deny_multiple_sessions")
		self.assertEqual(current_value, changed_value)

		changed_value = not current_value
		jingrow.db.set_single_value("System Settings", "deny_multiple_sessions", changed_value)
		current_value = jingrow.db.get_single_value("System Settings", "deny_multiple_sessions")
		self.assertEqual(current_value, changed_value)

	def test_none_no_set_value(self):
		jingrow.db.set_value("User", None, "middle_name", "test")
		with self.assertQueryCount(0):
			jingrow.db.set_value("User", None, "middle_name", "test")
			jingrow.db.set_value("User", "User", "middle_name", "test")

	def test_update_single_row_single_column(self):
		jingrow.db.set_value("ToDo", self.todo1.name, "description", "test_set_value change 1")
		updated_value = jingrow.db.get_value("ToDo", self.todo1.name, "description")
		self.assertEqual(updated_value, "test_set_value change 1")

	@patch("jingrow.db.set_single_value")
	def test_set_single_value_with_set_value(self, single_set):
		jingrow.db.set_value("Contact Us Settings", None, "country", "India")
		single_set.assert_called_once()

	def test_update_single_row_multiple_columns(self):
		description, status = "Upated by test_update_single_row_multiple_columns", "Closed"

		jingrow.db.set_value(
			"ToDo",
			self.todo1.name,
			{
				"description": description,
				"status": status,
			},
			update_modified=False,
		)

		updated_desciption, updated_status = jingrow.db.get_value(
			"ToDo", filters={"name": self.todo1.name}, fieldname=["description", "status"]
		)

		self.assertEqual(description, updated_desciption)
		self.assertEqual(status, updated_status)

	def test_update_multiple_rows_single_column(self):
		jingrow.db.set_value("ToDo", {"description": ("like", "%test_set_value%")}, "description", "change 2")

		self.assertEqual(jingrow.db.get_value("ToDo", self.todo1.name, "description"), "change 2")
		self.assertEqual(jingrow.db.get_value("ToDo", self.todo2.name, "description"), "change 2")

	def test_update_multiple_rows_multiple_columns(self):
		todos_to_update = jingrow.get_all(
			"ToDo",
			filters={"description": ("like", "%test_set_value%"), "status": ("!=", "Closed")},
			pluck="name",
		)

		jingrow.db.set_value(
			"ToDo",
			{"description": ("like", "%test_set_value%"), "status": ("!=", "Closed")},
			{"status": "Closed", "priority": "High"},
		)

		test_result = jingrow.get_all(
			"ToDo", filters={"name": ("in", todos_to_update)}, fields=["status", "priority"]
		)

		self.assertTrue(all(x for x in test_result if x["status"] == "Closed"))
		self.assertTrue(all(x for x in test_result if x["priority"] == "High"))

	def test_update_modified_options(self):
		self.todo2.reload()

		todo = self.todo2
		updated_description = f"{todo.description} - by `test_update_modified_options`"
		custom_modified = datetime.datetime.fromisoformat(add_days(now(), 10))
		custom_modified_by = "user_that_doesnt_exist@example.com"

		jingrow.db.set_value("ToDo", todo.name, "description", updated_description, update_modified=False)
		self.assertEqual(updated_description, jingrow.db.get_value("ToDo", todo.name, "description"))
		self.assertEqual(todo.modified, jingrow.db.get_value("ToDo", todo.name, "modified"))

		jingrow.db.set_value(
			"ToDo",
			todo.name,
			"description",
			"test_set_value change 1",
			modified=custom_modified,
			modified_by=custom_modified_by,
		)
		self.assertTupleEqual(
			(custom_modified, custom_modified_by),
			jingrow.db.get_value("ToDo", todo.name, ["modified", "modified_by"]),
		)

	def test_set_value(self):
		self.todo1.reload()

		jingrow.db.set_value(
			self.todo1.pagetype,
			self.todo1.name,
			"description",
			f"{self.todo1.description}-edit by `test_for_update`",
		)
		query = str(jingrow.db.last_query)

		if jingrow.conf.db_type == "postgres":
			from jingrow.database.postgres.database import modify_query

			self.assertTrue(modify_query("UPDATE `tabToDo` SET") in query)
		if jingrow.conf.db_type == "mariadb":
			self.assertTrue("UPDATE `tabToDo` SET" in query)

	def test_cleared_cache(self):
		self.todo2.reload()
		jingrow.get_cached_pg(self.todo2.pagetype, self.todo2.name)  # init cache

		description = f"{self.todo2.description}-edit by `test_cleared_cache`"

		jingrow.db.set_value(self.todo2.pagetype, self.todo2.name, "description", description)
		cached_pg = jingrow.get_cached_pg(self.todo2.pagetype, self.todo2.name)
		self.assertEqual(cached_pg.description, description)

	@classmethod
	def tearDownClass(cls):
		jingrow.db.rollback()


@run_only_if(db_type_is.POSTGRES)
class TestDDLCommandsPost(JingrowTestCase):
	test_table_name = "TestNotes"

	def setUp(self) -> None:
		jingrow.db.sql(
			f"""
			CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL, content text, PRIMARY KEY ("id"))
			"""
		)

	def tearDown(self) -> None:
		jingrow.db.sql(f'DROP TABLE "tab{self.test_table_name}"')
		self.test_table_name = "TestNotes"

	def test_rename(self) -> None:
		new_table_name = f"{self.test_table_name}_new"
		jingrow.db.rename_table(self.test_table_name, new_table_name)
		check_exists = jingrow.db.sql(
			f"""
			SELECT EXISTS (
			SELECT FROM information_schema.tables
			WHERE  table_name = 'tab{new_table_name}'
			);
			"""
		)
		self.assertTrue(check_exists[0][0])

		# * so this table is deleted after the rename
		self.test_table_name = new_table_name

	def test_describe(self) -> None:
		self.assertSequenceEqual([("id",), ("content",)], jingrow.db.describe(self.test_table_name))

	def test_change_type(self) -> None:
		from psycopg2.errors import DatatypeMismatch

		def get_table_description():
			return jingrow.db.sql(
				f"""
				SELECT
					table_name,
					column_name,
					data_type
				FROM
					information_schema.columns
				WHERE
					table_name = 'tab{self.test_table_name}'"""
			)

		# try changing from int to varchar
		jingrow.db.change_column_type(self.test_table_name, "id", "varchar(255)")
		self.assertIn("character varying", get_table_description()[0])

		# try changing from varchar to int
		try:
			jingrow.db.change_column_type(self.test_table_name, "id", "bigint")
		except DatatypeMismatch:
			jingrow.db.rollback()

		# try changing from varchar to int (using cast)
		jingrow.db.change_column_type(self.test_table_name, "id", "bigint", use_cast=True)
		self.assertIn("bigint", get_table_description()[0])

	def test_add_index(self) -> None:
		index_name = "test_index"
		jingrow.db.add_index(self.test_table_name, ["id", "content(50)"], index_name)
		indexs_in_table = jingrow.db.sql(
			f"""
			SELECT indexname
			FROM pg_indexes
			WHERE tablename = 'tab{self.test_table_name}'
			AND indexname = '{index_name}' ;
			""",
		)
		self.assertEqual(len(indexs_in_table), 1)

	def test_sequence_table_creation(self):
		from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype

		dt = new_pagetype("autoinc_dt_seq_test", autoname="autoincrement").insert(ignore_permissions=True)

		if jingrow.db.db_type == "postgres":
			self.assertTrue(
				jingrow.db.sql(
					"""select sequence_name FROM information_schema.sequences
				where sequence_name ilike 'autoinc_dt_seq_test%'"""
				)[0][0]
			)
		else:
			self.assertTrue(
				jingrow.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)

	def test_is(self):
		user = jingrow.qb.PageType("User")
		self.assertIn(
			"is not null", jingrow.db.get_values(user, filters={user.name: ("is", "set")}, run=False).lower()
		)
		self.assertIn(
			"is null", jingrow.db.get_values(user, filters={user.name: ("is", "not set")}, run=False).lower()
		)


@run_only_if(db_type_is.POSTGRES)
class TestTransactionManagement(JingrowTestCase):
	def test_create_proper_transactions(self):
		def _get_transaction_id():
			return jingrow.db.sql("select txid_current()", pluck=True)

		self.assertEqual(_get_transaction_id(), _get_transaction_id())

		jingrow.db.rollback()
		self.assertEqual(_get_transaction_id(), _get_transaction_id())

		jingrow.db.commit()
		self.assertEqual(_get_transaction_id(), _get_transaction_id())


# Treat same DB as replica for tests, a separate connection will be opened
class TestReplicaConnections(JingrowTestCase):
	def test_switching_to_replica(self):
		with patch.dict(jingrow.local.conf, {"read_from_replica": 1, "replica_host": "127.0.0.1"}):

			def db_id():
				return id(jingrow.local.db)

			write_connection = db_id()
			read_only_connection = None

			@jingrow.read_only()
			def outer():
				nonlocal read_only_connection
				read_only_connection = db_id()

				# A new connection should be opened
				self.assertNotEqual(read_only_connection, write_connection)
				inner()
				# calling nested read only function shouldn't change connection
				self.assertEqual(read_only_connection, db_id())

			@jingrow.read_only()
			def inner():
				# calling nested read only function shouldn't change connection
				self.assertEqual(read_only_connection, db_id())

			outer()
			self.assertEqual(write_connection, db_id())


class TestConcurrency(JingrowTestCase):
	@timeout(5, "There shouldn't be any lock wait")
	def test_skip_locking(self):
		with self.primary_connection():
			name = jingrow.db.get_value("User", "Administrator", for_update=True, skip_locked=True)
			self.assertEqual(name, "Administrator")

		with self.secondary_connection():
			name = jingrow.db.get_value("User", "Administrator", for_update=True, skip_locked=True)
			self.assertFalse(name)

	@timeout(5, "Lock timeout should have been 0")
	def test_no_wait(self):
		with self.primary_connection():
			name = jingrow.db.get_value("User", "Administrator", for_update=True)
			self.assertEqual(name, "Administrator")

		with self.secondary_connection():
			self.assertRaises(
				jingrow.QueryTimeoutError,
				lambda: jingrow.db.get_value("User", "Administrator", for_update=True, wait=False),
			)

	@timeout(5, "Deletion stuck on lock timeout")
	def test_delete_race_condition(self):
		note = jingrow.new_pg("Note")
		note.title = note.content = jingrow.generate_hash()
		note.insert()
		jingrow.db.commit()  # ensure that second connection can see the document

		with self.primary_connection():
			n1 = jingrow.get_pg(note.pagetype, note.name)
			n1.save()

		with self.secondary_connection():
			self.assertRaises(jingrow.QueryTimeoutError, jingrow.delete_pg, note.pagetype, note.name)


class TestSqlIterator(JingrowTestCase):
	def test_db_sql_iterator(self):
		test_queries = [
			"select * from `tabCountry` order by name",
			"select code from `tabCountry` order by name",
			"select code from `tabCountry` order by name limit 5",
		]

		for query in test_queries:
			self.assertEqual(
				jingrow.db.sql(query, as_dict=True),
				list(jingrow.db.sql(query, as_dict=True, as_iterator=True)),
				msg=f"{query=} results not same as iterator",
			)

			self.assertEqual(
				jingrow.db.sql(query, pluck=True),
				list(jingrow.db.sql(query, pluck=True, as_iterator=True)),
				msg=f"{query=} results not same as iterator",
			)

			self.assertEqual(
				jingrow.db.sql(query, as_list=True),
				list(jingrow.db.sql(query, as_list=True, as_iterator=True)),
				msg=f"{query=} results not same as iterator",
			)

	@run_only_if(db_type_is.MARIADB)
	def test_unbuffered_cursor(self):
		with jingrow.db.unbuffered_cursor():
			self.test_db_sql_iterator()
