Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 
 

314 рядки
9.2 KiB

  1. import re
  2. import frappe
  3. import psycopg2
  4. import psycopg2.extensions
  5. from six import string_types
  6. from frappe.utils import cstr
  7. from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
  8. from frappe.database.database import Database
  9. from frappe.database.postgres.schema import PostgresTable
  10. # cast decimals as floats
  11. DEC2FLOAT = psycopg2.extensions.new_type(
  12. psycopg2.extensions.DECIMAL.values,
  13. 'DEC2FLOAT',
  14. lambda value, curs: float(value) if value is not None else None)
  15. psycopg2.extensions.register_type(DEC2FLOAT)
  16. class PostgresDatabase(Database):
  17. ProgrammingError = psycopg2.ProgrammingError
  18. TableMissingError = psycopg2.ProgrammingError
  19. OperationalError = psycopg2.OperationalError
  20. InternalError = psycopg2.InternalError
  21. SQLError = psycopg2.ProgrammingError
  22. DataError = psycopg2.DataError
  23. InterfaceError = psycopg2.InterfaceError
  24. REGEX_CHARACTER = '~'
  25. def setup_type_map(self):
  26. self.db_type = 'postgres'
  27. self.type_map = {
  28. 'Currency': ('decimal', '18,6'),
  29. 'Int': ('bigint', None),
  30. 'Long Int': ('bigint', None),
  31. 'Float': ('decimal', '18,6'),
  32. 'Percent': ('decimal', '18,6'),
  33. 'Check': ('smallint', None),
  34. 'Small Text': ('text', ''),
  35. 'Long Text': ('text', ''),
  36. 'Code': ('text', ''),
  37. 'Text Editor': ('text', ''),
  38. 'Markdown Editor': ('text', ''),
  39. 'HTML Editor': ('text', ''),
  40. 'Date': ('date', ''),
  41. 'Datetime': ('timestamp', None),
  42. 'Time': ('time', '6'),
  43. 'Text': ('text', ''),
  44. 'Data': ('varchar', self.VARCHAR_LEN),
  45. 'Link': ('varchar', self.VARCHAR_LEN),
  46. 'Dynamic Link': ('varchar', self.VARCHAR_LEN),
  47. 'Password': ('text', ''),
  48. 'Select': ('varchar', self.VARCHAR_LEN),
  49. 'Rating': ('smallint', None),
  50. 'Read Only': ('varchar', self.VARCHAR_LEN),
  51. 'Attach': ('text', ''),
  52. 'Attach Image': ('text', ''),
  53. 'Signature': ('text', ''),
  54. 'Color': ('varchar', self.VARCHAR_LEN),
  55. 'Barcode': ('text', ''),
  56. 'Geolocation': ('text', ''),
  57. 'Duration': ('decimal', '18,6')
  58. }
  59. def get_connection(self):
  60. conn = psycopg2.connect("host='{}' dbname='{}' user='{}' password='{}' port={}".format(
  61. self.host, self.user, self.user, self.password, self.port
  62. ))
  63. conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # TODO: Remove this
  64. return conn
  65. def escape(self, s, percent=True):
  66. """Excape quotes and percent in given string."""
  67. if isinstance(s, bytes):
  68. s = s.decode('utf-8')
  69. if percent:
  70. s = s.replace("%", "%%")
  71. s = s.encode('utf-8')
  72. return str(psycopg2.extensions.QuotedString(s))
  73. def get_database_size(self):
  74. ''''Returns database size in MB'''
  75. db_size = self.sql("SELECT (pg_database_size(%s) / 1024 / 1024) as database_size",
  76. self.db_name, as_dict=True)
  77. return db_size[0].get('database_size')
  78. # pylint: disable=W0221
  79. def sql(self, *args, **kwargs):
  80. if args:
  81. # since tuple is immutable
  82. args = list(args)
  83. args[0] = modify_query(args[0])
  84. args = tuple(args)
  85. elif kwargs.get('query'):
  86. kwargs['query'] = modify_query(kwargs.get('query'))
  87. return super(PostgresDatabase, self).sql(*args, **kwargs)
  88. def get_tables(self):
  89. return [d[0] for d in self.sql("""select table_name
  90. from information_schema.tables
  91. where table_catalog='{0}'
  92. and table_type = 'BASE TABLE'
  93. and table_schema='{1}'""".format(frappe.conf.db_name, frappe.conf.get("db_schema", "public")))]
  94. def format_date(self, date):
  95. if not date:
  96. return '0001-01-01'
  97. if not isinstance(date, str):
  98. date = date.strftime('%Y-%m-%d')
  99. return date
  100. # column type
  101. @staticmethod
  102. def is_type_number(code):
  103. return code == psycopg2.NUMBER
  104. @staticmethod
  105. def is_type_datetime(code):
  106. return code == psycopg2.DATETIME
  107. # exception type
  108. @staticmethod
  109. def is_deadlocked(e):
  110. return e.pgcode == '40P01'
  111. @staticmethod
  112. def is_timedout(e):
  113. # http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
  114. return isinstance(e, psycopg2.extensions.QueryCanceledError)
  115. @staticmethod
  116. def is_table_missing(e):
  117. return getattr(e, 'pgcode', None) == '42P01'
  118. @staticmethod
  119. def is_missing_column(e):
  120. return getattr(e, 'pgcode', None) == '42703'
  121. @staticmethod
  122. def is_access_denied(e):
  123. return e.pgcode == '42501'
  124. @staticmethod
  125. def cant_drop_field_or_key(e):
  126. return e.pgcode.startswith('23')
  127. @staticmethod
  128. def is_duplicate_entry(e):
  129. return e.pgcode == '23505'
  130. @staticmethod
  131. def is_primary_key_violation(e):
  132. return e.pgcode == '23505' and '_pkey' in cstr(e.args[0])
  133. @staticmethod
  134. def is_unique_key_violation(e):
  135. return e.pgcode == '23505' and '_key' in cstr(e.args[0])
  136. @staticmethod
  137. def is_duplicate_fieldname(e):
  138. return e.pgcode == '42701'
  139. @staticmethod
  140. def is_data_too_long(e):
  141. return e.pgcode == '22001'
  142. def create_auth_table(self):
  143. self.sql_ddl("""create table if not exists "__Auth" (
  144. "doctype" VARCHAR(140) NOT NULL,
  145. "name" VARCHAR(255) NOT NULL,
  146. "fieldname" VARCHAR(140) NOT NULL,
  147. "password" TEXT NOT NULL,
  148. "encrypted" INT NOT NULL DEFAULT 0,
  149. PRIMARY KEY ("doctype", "name", "fieldname")
  150. )""")
  151. def create_global_search_table(self):
  152. if not '__global_search' in self.get_tables():
  153. self.sql('''create table "__global_search"(
  154. doctype varchar(100),
  155. name varchar({0}),
  156. title varchar({0}),
  157. content text,
  158. route varchar({0}),
  159. published int not null default 0,
  160. unique (doctype, name))'''.format(self.VARCHAR_LEN))
  161. def create_user_settings_table(self):
  162. self.sql_ddl("""create table if not exists "__UserSettings" (
  163. "user" VARCHAR(180) NOT NULL,
  164. "doctype" VARCHAR(180) NOT NULL,
  165. "data" TEXT,
  166. UNIQUE ("user", "doctype")
  167. )""")
  168. def create_help_table(self):
  169. self.sql('''CREATE TABLE "help"(
  170. "path" varchar(255),
  171. "content" text,
  172. "title" text,
  173. "intro" text,
  174. "full_path" text)''')
  175. self.sql('''CREATE INDEX IF NOT EXISTS "help_index" ON "help" ("path")''')
  176. def updatedb(self, doctype, meta=None):
  177. """
  178. Syncs a `DocType` to the table
  179. * creates if required
  180. * updates columns
  181. * updates indices
  182. """
  183. res = self.sql("select issingle from `tabDocType` where name='{}'".format(doctype))
  184. if not res:
  185. raise Exception('Wrong doctype {0} in updatedb'.format(doctype))
  186. if not res[0][0]:
  187. db_table = PostgresTable(doctype, meta)
  188. db_table.validate()
  189. self.commit()
  190. db_table.sync()
  191. self.begin()
  192. @staticmethod
  193. def get_on_duplicate_update(key='name'):
  194. if isinstance(key, list):
  195. key = '", "'.join(key)
  196. return 'ON CONFLICT ("{key}") DO UPDATE SET '.format(
  197. key=key
  198. )
  199. def check_transaction_status(self, query):
  200. pass
  201. def has_index(self, table_name, index_name):
  202. return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'
  203. and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name))
  204. def add_index(self, doctype, fields, index_name=None):
  205. """Creates an index with given fields if not already created.
  206. Index name will be `fieldname1_fieldname2_index`"""
  207. index_name = index_name or self.get_index_name(fields)
  208. table_name = 'tab' + doctype
  209. self.commit()
  210. self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
  211. def add_unique(self, doctype, fields, constraint_name=None):
  212. if isinstance(fields, string_types):
  213. fields = [fields]
  214. if not constraint_name:
  215. constraint_name = "unique_" + "_".join(fields)
  216. if not self.sql("""
  217. SELECT CONSTRAINT_NAME
  218. FROM information_schema.TABLE_CONSTRAINTS
  219. WHERE table_name=%s
  220. AND constraint_type='UNIQUE'
  221. AND CONSTRAINT_NAME=%s""",
  222. ('tab' + doctype, constraint_name)):
  223. self.commit()
  224. self.sql("""ALTER TABLE `tab%s`
  225. ADD CONSTRAINT %s UNIQUE (%s)""" % (doctype, constraint_name, ", ".join(fields)))
  226. def get_table_columns_description(self, table_name):
  227. """Returns list of column and its description"""
  228. # pylint: disable=W1401
  229. return self.sql('''
  230. SELECT a.column_name AS name,
  231. CASE LOWER(a.data_type)
  232. WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')')
  233. WHEN 'timestamp without time zone' THEN 'timestamp'
  234. ELSE a.data_type
  235. END AS type,
  236. COUNT(b.indexdef) AS Index,
  237. SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
  238. BOOL_OR(b.unique) AS unique
  239. FROM information_schema.columns a
  240. LEFT JOIN
  241. (SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique
  242. FROM pg_indexes
  243. WHERE tablename='{table_name}') b
  244. ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%')
  245. WHERE a.table_name = '{table_name}'
  246. GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;'''
  247. .format(table_name=table_name), as_dict=1)
  248. def get_database_list(self, target):
  249. return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]
  250. def modify_query(query):
  251. """"Modifies query according to the requirements of postgres"""
  252. # replace ` with " for definitions
  253. query = query.replace('`', '"')
  254. query = replace_locate_with_strpos(query)
  255. # select from requires ""
  256. if re.search('from tab', query, flags=re.IGNORECASE):
  257. query = re.sub('from tab([a-zA-Z]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
  258. return query
  259. def replace_locate_with_strpos(query):
  260. # strpos is the locate equivalent in postgres
  261. if re.search(r'locate\(', query, flags=re.IGNORECASE):
  262. query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
  263. return query