seitime-frappe/frappe/database/postgres/schema.py
Ritwik Puri b41379c78b
fix: misc fixes (integer primary keys) (#16307)
* fix: misc fixes

local.x gets resetted on every request so switched to a simple dict

simplified is_val_used in set_next_val function for sequences

* chore: use multisql for sequence methods

* fix: fields not updating on form

* minor(base_input): removed unnecessary branching in update_input

* chore: remove prints and rename autoincremented_status_map

* chore: added proper type hint + comment + formatting

* fix: added searching in cast_name rather than handling it manually

* fix: share condition query + test_build_match_conditions

* fix: add cast_name to more places

* test: test for sequence

* fix: sequence functions

* fix: inherit frappetestcase

* minor: attach sequence methods to db context local

* chore: update sequence function names in Database

use frappe.db for sequences in naming.py

* fix: convert filename to str (for autoincremented doctypes)

* chore: better regex for modifying values for postgres

* minor: allow changing name column type (if no data is present in the doctype)

* refactor: validate_autoname

converted it to a simple function
enabled changing autoincrement autoname from customize form

* fix: use sql_ddl for change_column_type in postgres

* fix: use not null constraint in postgres when changing name type

* fix(test): updated test_autoincremented_doctype_transition with transitioning when no data is present

* fix(test): updated test_cast_name

probably messed up during rebase

* fix(test): used rollback upon error in transaction for postgres

* chore: use frappe.db.x methods for sequences

* minor: use temporary sequences in test

* minor: use generate_hash for sequence naming in sequence tests

* chore: replace sequence imports with frappe.db.x

* chore: move out casting name fields to a separate method

* refactor: cast_name

more explicit cases for casts and added docstring

* fix: added space in test_cast_name

* chore: fix linter

* chore: better naming for can_change_name_column_type

* chore: add comment for autoincremented_site_status_map

* chore: update/add docstrings
2022-04-29 15:06:03 +05:30

176 lines
5.2 KiB
Python

import frappe
from frappe import _
from frappe.database.schema import DBTable, get_definition
from frappe.model import log_types
from frappe.utils import cint, flt
class PostgresTable(DBTable):
def create(self):
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"
additional_definitions = ""
# columns
column_defs = self.get_column_definitions()
if column_defs:
additional_definitions += ",\n".join(column_defs)
# child table columns
if self.meta.get("istable") or 0:
if column_defs:
additional_definitions += ",\n"
additional_definitions += ",\n".join(
(
f"parent varchar({varchar_len})",
f"parentfield varchar({varchar_len})",
f"parenttype varchar({varchar_len})",
)
)
# 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
frappe.db.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_column},
creation timestamp(6),
modified timestamp(6),
modified_by varchar({varchar_len}),
owner varchar({varchar_len}),
docstatus smallint not null default '0',
idx bigint not null default '0',
{additional_definitions}
)"""
)
self.create_indexes()
frappe.db.commit()
def create_indexes(self):
create_index_query = ""
for key, col in self.columns.items():
if (
col.set_index
and col.fieldtype in frappe.db.type_map
and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext")
):
create_index_query += (
'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
index_name=col.fieldname, table_name=self.table_name, field=col.fieldname
)
)
if create_index_query:
# nosemgrep
frappe.db.sql(create_index_query)
def alter(self):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
query = []
for col in self.add_column:
query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.change_type:
using_clause = ""
if col.fieldtype in ("Datetime"):
# The USING option of SET DATA TYPE can actually specify any expression
# involving the old values of the row
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
using_clause = "USING {}::timestamp without time zone".format(col.fieldname)
elif col.fieldtype in ("Check"):
using_clause = "USING {}::smallint".format(col.fieldname)
query.append(
"ALTER COLUMN `{0}` TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause,
)
)
for col in self.set_default:
if col.fieldname == "name":
continue
if col.fieldtype in ("Check", "Int"):
col_default = cint(col.default)
elif col.fieldtype in ("Currency", "Float", "Percent"):
col_default = flt(col.default)
elif not col.default:
col_default = "NULL"
else:
col_default = "{}".format(frappe.db.escape(col.default))
query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default))
create_contraint_query = ""
for col in self.add_index:
# if index key not exists
create_contraint_query += (
'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
index_name=col.fieldname, table_name=self.table_name, field=col.fieldname
)
)
for col in self.add_unique:
# if index key not exists
create_contraint_query += (
'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format(
index_name=col.fieldname, table_name=self.table_name, field=col.fieldname
)
)
drop_contraint_query = ""
for col in self.drop_index:
# primary key
if col.fieldname != "name":
# if index key exists
drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
for col in self.drop_unique:
# primary key
if col.fieldname != "name":
# if index key exists
drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname)
try:
if query:
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query))
# nosemgrep
frappe.db.sql(final_alter_query)
if create_contraint_query:
# nosemgrep
frappe.db.sql(create_contraint_query)
if drop_contraint_query:
# nosemgrep
frappe.db.sql(drop_contraint_query)
except Exception as e:
# sanitize
if frappe.db.is_duplicate_fieldname(e):
frappe.throw(str(e))
elif frappe.db.is_duplicate_entry(e):
fieldname = str(e).split("'")[-2]
frappe.throw(
_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
fieldname, self.table_name
)
)
else:
raise e