public inbox for [email protected]
help / color / mirror / Atom feedFrom: Murtuza Zabuawala <[email protected]>
To: pgadmin-hackers <[email protected]>
Subject: [pgAdmin4][RM#3140] Add service parameter
Date: Fri, 9 Mar 2018 17:17:36 +0530
Message-ID: <CAKKotZSrYHCypG0rfsD9DHCoE8-c+XAvBnU3vS8LMDfPqSrC1Q@mail.gmail.com> (raw)
Hi,
PFA patch to add service parameter in server dialog.
- Docs updated
- Test case added for Service ID parameter
Please note,
I have extracted Connection class and Server manager class from our own
custom Psycopg2 driver module.
Patch also covers RM#3120
Please review.
--
Regards,
Murtuza Zabuawala
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
[application/octet-stream] RM_3140.diff (257.4K, 3-RM_3140.diff)
download | inline diff:
diff --git a/docs/en_US/images/server_advanced.png b/docs/en_US/images/server_advanced.png
index 10f1809a364579a39b2fe1911391568260b379b8..2291d95f4378a21554a8a668a6f52ea0a485f485 100644
GIT binary patch
literal 55652
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
Q0g%5h5(?rKqI&-SA3I!p=l}o!
literal 34067
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
diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst
index 24a726b..6ac72a6 100644
--- a/docs/en_US/server_dialog.rst
+++ b/docs/en_US/server_dialog.rst
@@ -62,6 +62,7 @@ Use the fields in the *Advanced* tab to configure a connection:
* Specify the IP address of the server host in the *Host address* field. Using this field to specify the host IP address may save time by avoiding a DNS lookup on connection, but it may be useful to specify both a host name and address when using Kerberos, GSSAPI, or SSPI authentication methods, as well as for verify-full SSL certificate verification.
* Use the *DB restriction* field to provide a SQL restriction that will be used against the pg_database table to limit the databases that you see. For example, you might enter: *live_db test_db* so that only live_db and test_db are shown in the pgAdmin browser. Separate entries with a comma or tab as you type.
* Use the *Password File* field to specify the location of a password file (.pgpass). A .pgpass file allows a user to login without providing a password when they connect. For more information, see `Section 33.15 of the Postgres documentation <http://www.postgresql.org/docs/current/static/libpq-pgpass.html>`_.
+* Use the *Service ID* field to specify the service name. For more information, see `Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/10/static/libpq-pgservice.html>`_.
*NOTE:* The password file option is only supported when pgAdmin is using libpq v10.0 or later to connect to the server.
diff --git a/web/migrations/versions/50aad68f99c2_.py b/web/migrations/versions/50aad68f99c2_.py
new file mode 100644
index 0000000..f8c97f2
--- /dev/null
+++ b/web/migrations/versions/50aad68f99c2_.py
@@ -0,0 +1,82 @@
+
+"""Added service field option in server table (RM#3140)
+
+Revision ID: 50aad68f99c2
+Revises: 02b9dccdcfcb
+Create Date: 2018-03-07 11:53:57.584280
+
+"""
+from pgadmin.model import db
+
+
+# revision identifiers, used by Alembic.
+revision = '50aad68f99c2'
+down_revision = '02b9dccdcfcb'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # To Save previous data
+ db.engine.execute("ALTER TABLE server RENAME TO server_old")
+
+ # With service file some fields won't be mandatory as user can provide
+ # them using service file. Removed NOT NULL constraint from few columns
+ db.engine.execute("""
+ CREATE TABLE server (
+ id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ servergroup_id INTEGER NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ host VARCHAR(128),
+ port INTEGER NOT NULL CHECK(port >= 1024 AND port <= 65534),
+ maintenance_db VARCHAR(64),
+ username VARCHAR(64) NOT NULL,
+ password VARCHAR(64),
+ role VARCHAR(64),
+ ssl_mode VARCHAR(16) NOT NULL CHECK(ssl_mode IN
+ ( 'allow' , 'prefer' , 'require' , 'disable' ,
+ 'verify-ca' , 'verify-full' )
+ ),
+ comment VARCHAR(1024),
+ discovery_id VARCHAR(128),
+ hostaddr TEXT(1024),
+ db_res TEXT,
+ passfile TEXT,
+ sslcert TEXT,
+ sslkey TEXT,
+ sslrootcert TEXT,
+ sslcrl TEXT,
+ sslcompression INTEGER DEFAULT 0,
+ bgcolor TEXT(10),
+ fgcolor TEXT(10),
+ PRIMARY KEY(id),
+ FOREIGN KEY(user_id) REFERENCES user(id),
+ FOREIGN KEY(servergroup_id) REFERENCES servergroup(id)
+ )
+ """)
+
+ # Copy old data again into table
+ db.engine.execute("""
+ INSERT INTO server (
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ ) SELECT
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ FROM server_old""")
+
+ # Remove old data
+ db.engine.execute("DROP TABLE server_old")
+
+ # Add column for Service ID
+ db.engine.execute(
+ 'ALTER TABLE server ADD COLUMN service TEXT'
+ )
+
+def downgrade():
+ pass
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index dfa9d62..fecb66d 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -478,7 +478,8 @@ class ServerNode(PGChildNodeView):
'sslcrl': 'sslcrl',
'sslcompression': 'sslcompression',
'bgcolor': 'bgcolor',
- 'fgcolor': 'fgcolor'
+ 'fgcolor': 'fgcolor',
+ 'service': 'service'
}
disp_lbl = {
@@ -515,7 +516,7 @@ class ServerNode(PGChildNodeView):
if connected:
for arg in (
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode',
- 'role'
+ 'role', 'service'
):
if arg in data:
return forbidden(
@@ -663,7 +664,8 @@ class ServerNode(PGChildNodeView):
'sslrootcert': server.sslrootcert if is_ssl else None,
'sslcrl': server.sslcrl if is_ssl else None,
'sslcompression': True if is_ssl and server.sslcompression
- else False
+ else False,
+ 'service': server.service if server.service else None
}
)
@@ -672,18 +674,22 @@ class ServerNode(PGChildNodeView):
"""Add a server node to the settings database"""
required_args = [
u'name',
- u'host',
u'port',
- u'db',
- u'username',
u'sslmode',
- u'role'
+ u'username'
]
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
+ # Some fields can be provided with service file so they are optional
+ if 'service' in data and not data['service']:
+ required_args.extend([
+ u'host',
+ u'db',
+ u'role'
+ ])
for arg in required_args:
if arg not in data:
return make_json_response(
@@ -711,29 +717,26 @@ class ServerNode(PGChildNodeView):
try:
server = Server(
user_id=current_user.id,
- servergroup_id=data[u'gid'] if u'gid' in data else gid,
- name=data[u'name'],
- host=data[u'host'],
- hostaddr=data[u'hostaddr'] if u'hostaddr' in data else None,
- port=data[u'port'],
- maintenance_db=data[u'db'],
- username=data[u'username'],
- ssl_mode=data[u'sslmode'],
- comment=data[u'comment'] if u'comment' in data else None,
- role=data[u'role'] if u'role' in data else None,
+ servergroup_id=data.get('gid', gid),
+ name=data.get('name'),
+ host=data.get('host', None),
+ hostaddr=data.get('hostaddr', None),
+ port=data.get('port'),
+ maintenance_db=data.get('db', None),
+ username=data.get('username'),
+ ssl_mode=data.get('sslmode'),
+ comment=data.get('comment', None),
+ role=data.get('role', None),
db_res=','.join(data[u'db_res'])
- if u'db_res' in data
- else None,
- sslcert=data['sslcert'] if is_ssl else None,
- sslkey=data['sslkey'] if is_ssl else None,
- sslrootcert=data['sslrootcert'] if is_ssl else None,
- sslcrl=data['sslcrl'] if is_ssl else None,
+ if u'db_res' in data else None,
+ sslcert=data.get('sslcert', None),
+ sslkey=data.get('sslkey', None),
+ sslrootcert=data.get('sslrootcert', None),
+ sslcrl=data.get('sslcrl', None),
sslcompression=1 if is_ssl and data['sslcompression'] else 0,
- bgcolor=data['bgcolor'] if u'bgcolor' in data
- else None,
- fgcolor=data['fgcolor'] if u'fgcolor' in data
- else None
-
+ bgcolor=data.get('bgcolor', None),
+ fgcolor=data.get('fgcolor', None),
+ service=data.get('service', None)
)
db.session.add(server)
db.session.commit()
@@ -930,7 +933,7 @@ class ServerNode(PGChildNodeView):
if 'password' not in data:
conn_passwd = getattr(conn, 'password', None)
if conn_passwd is None and server.password is None and \
- server.passfile is None:
+ server.passfile is None and server.service is None:
# Return the password template in case password is not
# provided, or password has not been saved earlier.
return make_json_response(
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 9932808..bedb38d 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -665,6 +665,7 @@ define('pgadmin.node.server', [
sslkey: undefined,
sslrootcert: undefined,
sslcrl: undefined,
+ service: undefined,
},
// Default values!
initialize: function(attrs, args) {
@@ -841,12 +842,18 @@ define('pgadmin.node.server', [
var passfile = m.get('passfile');
return !_.isUndefined(passfile) && !_.isNull(passfile);
},
+ },{
+ id: 'service', label: gettext('Service ID'), type: 'text',
+ mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
+ group: gettext('Advanced'),
}],
validate: function() {
var err = {},
errmsg,
self = this;
+ var service_id = this.get('service');
+
var check_for_empty = function(id, msg) {
var v = self.get(id);
if (
@@ -903,26 +910,41 @@ define('pgadmin.node.server', [
}
check_for_empty('name', gettext('Name must be specified.'));
- if (check_for_empty(
- 'host', gettext('Either Host name or Host address must be specified.')
- ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
- errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ // If no service id then only check
+ if (
+ _.isUndefined(service_id) || _.isNull(service_id) ||
+ String(service_id).replace(/^\s+|\s+$/g, '') == ''
+ ) {
+ if (check_for_empty(
+ 'host', gettext('Either Host name or Host address must be specified.')
+ ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
+ errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ } else {
+ errmsg = undefined;
+ delete err['host'];
+ delete err['hostaddr'];
+ }
+
+ check_for_empty(
+ 'db', gettext('Maintenance database must be specified.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
} else {
- errmsg = undefined;
- delete err['host'];
- delete err['hostaddr'];
+ _.each(['host', 'hostaddr', 'db'], (item) => {
+ self.errorModel.unset(item);
+ });
}
check_for_empty(
- 'db', gettext('Maintenance database must be specified.')
- );
- check_for_empty(
'username', gettext('Username must be specified.')
);
check_for_empty('port', gettext('Port must be specified.'));
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
+
this.errorModel.set(err);
if (_.size(err)) {
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
new file mode 100644
index 0000000..3b03d49
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
@@ -0,0 +1,47 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import json
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+
+
+class ServersWithServiceIDAddTestCase(BaseTestGenerator):
+ """ This class will add the servers under default server group. """
+
+ scenarios = [
+ # Fetch the default url for server object
+ (
+ 'Default Server Node url', dict(
+ url='/browser/server/obj/'
+ )
+ )
+ ]
+
+ def setUp(self):
+ pass
+
+ def runTest(self):
+ """ This function will add the server under default server group."""
+ url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+ # Add service name in the config
+ self.server['service'] = "TestDB"
+ response = self.tester.post(
+ url,
+ data=json.dumps(self.server),
+ content_type='html/json'
+ )
+ self.assertEquals(response.status_code, 200)
+ response_data = json.loads(response.data.decode('utf-8'))
+ self.server_id = response_data['node']['_id']
+
+ def tearDown(self):
+ """This function delete the server from SQLite """
+ utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index 674f945..11bc9f0 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -107,13 +107,13 @@ class Server(db.Model):
nullable=False
)
name = db.Column(db.String(128), nullable=False)
- host = db.Column(db.String(128), nullable=False)
+ host = db.Column(db.String(128), nullable=True)
hostaddr = db.Column(db.String(128), nullable=True)
port = db.Column(
db.Integer(),
db.CheckConstraint('port >= 1024 AND port <= 65534'),
nullable=False)
- maintenance_db = db.Column(db.String(64), nullable=False)
+ maintenance_db = db.Column(db.String(64), nullable=True)
username = db.Column(db.String(64), nullable=False)
password = db.Column(db.String(64), nullable=True)
role = db.Column(db.String(64), nullable=True)
@@ -144,6 +144,7 @@ class Server(db.Model):
)
bgcolor = db.Column(db.Text(10), nullable=True)
fgcolor = db.Column(db.Text(10), nullable=True)
+ service = db.Column(db.Text(), nullable=True)
class ModulePreference(db.Model):
diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py
index 941a694..95a49fb 100644
--- a/web/pgadmin/utils/driver/psycopg2/__init__.py
+++ b/web/pgadmin/utils/driver/psycopg2/__init__.py
@@ -8,1985 +8,23 @@
##########################################################################
"""
-Implementation of Connection, ServerManager and Driver classes using the
-psycopg2. It is a wrapper around the actual psycopg2 driver, and connection
+Implementation of Driver class
+It is a wrapper around the actual psycopg2 driver, and connection
object.
-"""
+"""
import datetime
-import os
-import random
-import select
-import sys
-
-import simplejson as json
-import psycopg2
-from flask import g, current_app, session
+from flask import session
from flask_babel import gettext
-from flask_security import current_user
-from pgadmin.utils.crypto import decrypt
-from psycopg2.extensions import adapt, encodings
+import psycopg2
+from psycopg2.extensions import adapt
import config
from pgadmin.model import Server, User
-from pgadmin.utils.exception import ConnectionLost
-from pgadmin.utils import get_complete_file_path
from .keywords import ScanKeyword
-from ..abstract import BaseDriver, BaseConnection
-from .cursor import DictCursor
-from .typecast import register_global_typecasters, \
- register_string_typecasters, register_binary_typecasters, \
- register_array_to_string_typecasters, ALL_JSON_TYPES
-from collections import deque
-
-
-if sys.version_info < (3,):
- # Python2 in-built csv module do not handle unicode
- # backports.csv module ported from PY3 csv module for unicode handling
- from backports import csv
- from StringIO import StringIO
- IS_PY2 = True
-else:
- from io import StringIO
- import csv
- IS_PY2 = False
-
-_ = gettext
-
-
-# Register global type caster which will be applicable to all connections.
-register_global_typecasters()
-
-
-class Connection(BaseConnection):
- """
- class Connection(object)
-
- A wrapper class, which wraps the psycopg2 connection object, and
- delegate the execution to the actual connection object, when required.
-
- Methods:
- -------
- * connect(**kwargs)
- - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
- driver
-
- * execute_scalar(query, params, formatted_exception_msg)
- - Execute the given query and returns single datum result
-
- * execute_async(query, params, formatted_exception_msg)
- - Execute the given query asynchronously and returns result.
-
- * execute_void(query, params, formatted_exception_msg)
- - Execute the given query with no result.
-
- * execute_2darray(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as a 2 dimensional
- array.
-
- * execute_dict(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as an array of dict
- (column name -> value) format.
-
- * connected()
- - Get the status of the connection.
- Returns True if connected, otherwise False.
-
- * reset()
- - Reconnect the database server (if possible)
-
- * transaction_status()
- - Transaction Status
-
- * ping()
- - Ping the server.
-
- * _release()
- - Release the connection object of psycopg2
-
- * _reconnect()
- - Attempt to reconnect to the database
-
- * _wait(conn)
- - This method is used to wait for asynchronous connection. This is a
- blocking call.
-
- * _wait_timeout(conn)
- - This method is used to wait for asynchronous connection with timeout.
- This is a non blocking call.
-
- * poll(formatted_exception_msg)
- - This method is used to poll the data of query running on asynchronous
- connection.
-
- * status_message()
- - Returns the status message returned by the last command executed on
- the server.
-
- * rows_affected()
- - Returns the no of rows affected by the last command executed on
- the server.
-
- * cancel_transaction(conn_id, did=None)
- - This method is used to cancel the transaction for the
- specified connection id and database id.
-
- * messages()
- - Returns the list of messages/notices sends from the PostgreSQL database
- server.
-
- * _formatted_exception_msg(exception_obj, formatted_msg)
- - This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- """
-
- def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
- use_binary_placeholder=False, array_to_string=False):
- assert (manager is not None)
- assert (conn_id is not None)
-
- self.conn_id = conn_id
- self.manager = manager
- self.db = db if db is not None else manager.db
- self.conn = None
- self.auto_reconnect = auto_reconnect
- self.async = async
- self.__async_cursor = None
- self.__async_query_id = None
- self.__backend_pid = None
- self.execution_aborted = False
- self.row_count = 0
- self.__notices = None
- self.password = None
- # This flag indicates the connection status (connected/disconnected).
- self.wasConnected = False
- # This flag indicates the connection reconnecting status.
- self.reconnecting = False
- self.use_binary_placeholder = use_binary_placeholder
- self.array_to_string = array_to_string
-
- super(Connection, self).__init__()
-
- def as_dict(self):
- """
- Returns the dictionary object representing this object.
- """
- # In case, it cannot be auto reconnectable, or already been released,
- # then we will return None.
- if not self.auto_reconnect and not self.conn:
- return None
-
- res = dict()
- res['conn_id'] = self.conn_id
- res['database'] = self.db
- res['async'] = self.async
- res['wasConnected'] = self.wasConnected
- res['auto_reconnect'] = self.auto_reconnect
- res['use_binary_placeholder'] = self.use_binary_placeholder
- res['array_to_string'] = self.array_to_string
-
- return res
-
- def __repr__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def __str__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def connect(self, **kwargs):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- else:
- return True, None
-
- pg_conn = None
- password = None
- passfile = None
- mgr = self.manager
-
- encpass = kwargs['password'] if 'password' in kwargs else None
- passfile = kwargs['passfile'] if 'passfile' in kwargs else None
-
- if encpass is None:
- encpass = self.password or getattr(mgr, 'password', None)
-
- # Reset the existing connection password
- if self.reconnecting is not False:
- self.password = None
-
- if encpass:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- try:
- password = decrypt(encpass, user.password)
- # Handling of non ascii password (Python2)
- if hasattr(str, 'decode'):
- password = password.decode('utf-8').encode('utf-8')
- # password is in bytes, for python3 we need it in string
- elif isinstance(password, bytes):
- password = password.decode()
-
- except Exception as e:
- current_app.logger.exception(e)
- return False, \
- _(
- "Failed to decrypt the saved password.\nError: {0}"
- ).format(str(e))
-
- # If no password credential is found then connect request might
- # come from Query tool, ViewData grid, debugger etc tools.
- # we will check for pgpass file availability from connection manager
- # if it's present then we will use it
- if not password and not encpass and not passfile:
- passfile = mgr.passfile if mgr.passfile else None
-
- try:
- if hasattr(str, 'decode'):
- database = self.db.encode('utf-8')
- user = mgr.user.encode('utf-8')
- conn_id = self.conn_id.encode('utf-8')
- else:
- database = self.db
- user = mgr.user
- conn_id = self.conn_id
-
- import os
- os.environ['PGAPPNAME'] = '{0} - {1}'.format(
- config.APP_NAME, conn_id)
-
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=database,
- user=user,
- password=password,
- async=self.async,
- passfile=get_complete_file_path(passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- # If connection is asynchronous then we will have to wait
- # until the connection is ready to use.
- if self.async == 1:
- self._wait(pg_conn)
-
- except psycopg2.Error as e:
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- current_app.logger.info(
- u"Failed to connect to the database server(#{server_id}) for "
- u"connection ({conn_id}) with error message as below"
- u":{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
- )
- )
- return False, msg
-
- # Overwrite connection notice attr to support
- # more than 50 notices at a time
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.wasConnected = True
- try:
- status, msg = self._initialize(conn_id, **kwargs)
- except Exception as e:
- current_app.logger.exception(e)
- self.conn = None
- if not self.reconnecting:
- self.wasConnected = False
- raise e
-
- if status:
- mgr._update_password(encpass)
- else:
- if not self.reconnecting:
- self.wasConnected = False
-
- return status, msg
-
- def _initialize(self, conn_id, **kwargs):
- self.execution_aborted = False
- self.__backend_pid = self.conn.get_backend_pid()
-
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- status, cur = self.__cursor()
- formatted_exception_msg = self._formatted_exception_msg
- mgr = self.manager
-
- def _execute(cur, query, params=None):
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- return formatted_exception_msg(pe, False)
- return None
-
- # autocommit flag does not work with asynchronous connections.
- # By default asynchronous connection runs in autocommit mode.
- if self.async == 0:
- if 'autocommit' in kwargs and kwargs['autocommit'] is False:
- self.conn.autocommit = False
- else:
- self.conn.autocommit = True
-
- register_string_typecasters(self.conn)
-
- if self.array_to_string:
- register_array_to_string_typecasters(self.conn)
-
- # Register type casters for binary data only after registering array to
- # string type casters.
- if self.use_binary_placeholder:
- register_binary_typecasters(self.conn)
-
- status = _execute(cur, "SET DateStyle=ISO;"
- "SET client_min_messages=notice;"
- "SET bytea_output=escape;"
- "SET client_encoding='UNICODE';")
-
- if status is not None:
- self.conn.close()
- self.conn = None
-
- return False, status
-
- if mgr.role:
- status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
-
- if status is not None:
- self.conn.close()
- self.conn = None
- current_app.logger.error(
- "Connect to the database server (#{server_id}) for "
- "connection ({conn_id}), but - failed to setup the role "
- "with error message as below:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status
- )
- )
- return False, \
- _(
- "Failed to setup the role with error message:\n{0}"
- ).format(status)
-
- if mgr.ver is None:
- status = _execute(cur, "SELECT version()")
-
- if status is not None:
- self.conn.close()
- self.conn = None
- self.wasConnected = False
- current_app.logger.error(
- "Failed to fetch the version information on the "
- "established connection to the database server "
- "(#{server_id}) for '{conn_id}' with below error "
- "message:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status)
- )
- return False, status
-
- if cur.rowcount > 0:
- row = cur.fetchmany(1)[0]
- mgr.ver = row['version']
- mgr.sversion = self.conn.server_version
-
- status = _execute(cur, """
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.datname = current_database()""")
-
- if status is None:
- mgr.db_info = mgr.db_info or dict()
- if cur.rowcount > 0:
- res = cur.fetchmany(1)[0]
- mgr.db_info[res['did']] = res.copy()
-
- # We do not have database oid for the maintenance database.
- if len(mgr.db_info) == 1:
- mgr.did = res['did']
-
- status = _execute(cur, """
-SELECT
- oid as id, rolname as name, rolsuper as is_superuser,
- rolcreaterole as can_create_role, rolcreatedb as can_create_db
-FROM
- pg_catalog.pg_roles
-WHERE
- rolname = current_user""")
-
- if status is None:
- mgr.user_info = dict()
- if cur.rowcount > 0:
- mgr.user_info = cur.fetchmany(1)[0]
-
- if 'password' in kwargs:
- mgr.password = kwargs['password']
-
- server_types = None
- if 'server_types' in kwargs and isinstance(
- kwargs['server_types'], list):
- server_types = mgr.server_types = kwargs['server_types']
-
- if server_types is None:
- from pgadmin.browser.server_groups.servers.types import ServerType
- server_types = ServerType.types()
-
- for st in server_types:
- if st.instanceOf(mgr.ver):
- mgr.server_type = st.stype
- mgr.server_cls = st
- break
-
- mgr.update_session()
-
- return True, None
-
- def __cursor(self, server_cursor=False):
- if self.wasConnected is False:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- cur = getattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- if self.connected() and cur and not cur.closed:
- if not server_cursor or (server_cursor and cur.name):
- return True, cur
-
- if not self.connected():
- errmsg = ""
-
- current_app.logger.warning(
- "Connection to database server (#{server_id}) for the "
- "connection - '{conn_id}' has been lost.".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
-
- if self.auto_reconnect and not self.reconnecting:
- self.__attempt_execution_reconnect(None)
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- try:
- if server_cursor:
- # Providing name to cursor will create server side cursor.
- cursor_name = "CURSOR:{0}".format(self.conn_id)
- cur = self.conn.cursor(
- name=cursor_name, cursor_factory=DictCursor
- )
- else:
- cur = self.conn.cursor(cursor_factory=DictCursor)
- except psycopg2.Error as pe:
- current_app.logger.exception(pe)
- errmsg = gettext(
- "Failed to create cursor for psycopg2 connection with error "
- "message for the server#{1}:{2}:\n{0}"
- ).format(
- str(pe), self.manager.sid, self.db
- )
-
- current_app.logger.error(errmsg)
- if self.conn.closed:
- self.conn = None
- if self.auto_reconnect and not self.reconnecting:
- current_app.logger.info(
- gettext(
- "Attempting to reconnect to the database server "
- "(#{server_id}) for the connection - '{conn_id}'."
- ).format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- return self.__attempt_execution_reconnect(
- self.__cursor, server_cursor
- )
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:'
- else self.conn_id[5:]
- )
-
- setattr(
- g, "{0}#{1}".format(
- self.manager.sid, self.conn_id.encode('utf-8')
- ), cur
- )
-
- return True, cur
-
- def __internal_blocking_execute(self, cur, query, params):
- """
- This function executes the query using cursor's execute function,
- but in case of asynchronous connection we need to wait for the
- transaction to be completed. If self.async is 1 then it is a
- blocking call.
-
- Args:
- cur: Cursor object
- query: SQL query to run.
- params: Extra parameters
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- cur.execute(query, params)
- if self.async == 1:
- self._wait(cur.connection)
-
- def execute_on_server_as_csv(self,
- query, params=None,
- formatted_exception_msg=False,
- records=2000):
- """
- To fetch query result and generate CSV output
-
- Args:
- query: SQL
- params: Additional parameters
- formatted_exception_msg: For exception
- records: Number of initial records
- Returns:
- Generator response
- """
- status, cur = self.__cursor(server_cursor=True)
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- if IS_PY2 and type(query) == unicode:
- query = query.encode('utf-8')
-
- current_app.logger.log(
- 25,
- u"Execute (with server cursor) for server #{server_id} - "
- u"{conn_id} (Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8') if
- sys.version_info < (3,) else query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"failed to execute query ((with server cursor) "
- u"for the server #{server_id} - {conn_id} "
- u"(query-id: {query_id}):\nerror message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- def handle_json_data(json_columns, results):
- """
- [ This is only for Python2.x]
- This function will be useful to handle json data types.
- We will dump json data as proper json instead of unicode values
-
- Args:
- json_columns: Columns which contains json data
- results: Query result
-
- Returns:
- results
- """
- # Only if Python2 and there are columns with JSON type
- if IS_PY2 and len(json_columns) > 0:
- temp_results = []
- for row in results:
- res = dict()
- for k, v in row.items():
- if k in json_columns:
- res[k] = json.dumps(v)
- else:
- res[k] = v
- temp_results.append(res)
- results = temp_results
- return results
-
- def convert_keys_to_unicode(results, conn_encoding):
- """
- [ This is only for Python2.x]
- We need to convert all keys to unicode as psycopg2
- sends them as string
-
- Args:
- res: Query result set from psycopg2
- conn_encoding: Connection encoding
-
- Returns:
- Result set (With all the keys converted to unicode)
- """
- new_results = []
- for row in results:
- new_results.append(
- dict([(k.decode(conn_encoding), v)
- for k, v in row.items()])
- )
- return new_results
-
- def gen(quote='strings', quote_char="'", field_separator=','):
-
- results = cur.fetchmany(records)
- if not results:
- if not cur.closed:
- cur.close()
- yield gettext('The query executed did not return any data.')
- return
-
- header = []
- json_columns = []
- conn_encoding = cur.connection.encoding
-
- for c in cur.ordered_description():
- # This is to handle the case in which column name is non-ascii
- column_name = c.to_dict()['name']
- if IS_PY2:
- column_name = column_name.decode(conn_encoding)
- header.append(column_name)
- if c.to_dict()['type_code'] in ALL_JSON_TYPES:
- json_columns.append(column_name)
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- res_io = StringIO()
-
- if quote == 'strings':
- quote = csv.QUOTE_NONNUMERIC
- elif quote == 'all':
- quote = csv.QUOTE_ALL
- else:
- quote = csv.QUOTE_NONE
-
- if hasattr(str, 'decode'):
- # Decode the field_separator
- try:
- field_separator = field_separator.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- # Decode the quote_char
- try:
- quote_char = quote_char.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- csv_writer.writeheader()
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
-
- yield res_io.getvalue()
-
- while True:
- results = cur.fetchmany(records)
-
- if not results:
- if not cur.closed:
- cur.close()
- break
- res_io = StringIO()
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
- yield res_io.getvalue()
-
- return True, gen
-
- def execute_scalar(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_scalar) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- res = cur.fetchone()
- if len(res) > 0:
- return True, res[0]
-
- return True, None
-
- def execute_async(self, query, params=None, formatted_exception_msg=True):
- """
- This function executes the given query asynchronously and returns
- result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- self.__async_cursor = None
- status, cur = self.__cursor()
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- query_id=query_id
- )
- )
-
- try:
- self.__notices = []
- self.execution_aborted = False
- cur.execute(query, params)
- res = self._wait_timeout(cur.connection)
- except psycopg2.Error as pe:
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_async) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- errmsg=errmsg,
- query_id=query_id
- )
- )
-
- if self.is_disconnected(pe):
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- return False, errmsg
-
- self.__async_cursor = cur
- self.__async_query_id = query_id
-
- return True, res
-
- def execute_void(self, query, params=None, formatted_exception_msg=False):
- """
- This function executes the given query with no result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
-
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_void, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_void) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
-
- return True, None
-
- def __attempt_execution_reconnect(self, fn, *args, **kwargs):
- self.reconnecting = True
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
- try:
- status, res = self.connect()
- if status:
- if fn:
- status, res = fn(*args, **kwargs)
- self.reconnecting = False
- return status, res
- except Exception as e:
- current_app.logger.exception(e)
- self.reconnecting = False
-
- current_app.warning(
- "Failed to reconnect the database server "
- "(#{server_id})".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- self.reconnecting = False
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- def execute_2darray(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
-
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (2darray) for server #{server_id} - {conn_id} "
- u"(Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and \
- not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_2darray, query, params,
- formatted_exception_msg
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_2darray) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(row)
-
- return True, {'columns': columns, 'rows': rows}
-
- def execute_dict(self, query, params=None, formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_dict) for the server "
- u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query_id=query_id,
- errmsg=errmsg
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(dict(row))
-
- return True, {'columns': columns, 'rows': rows}
-
- def async_fetchmany_2darray(self, records=2000,
- formatted_exception_msg=False):
- """
- User should poll and check if status is ASYNC_OK before calling this
- function
- Args:
- records: no of records to fetch. use -1 to fetchall.
- formatted_exception_msg:
-
- Returns:
-
- """
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- if self.conn.isexecuting():
- return False, gettext(
- "Asynchronous query execution/operation underway."
- )
-
- if self.row_count > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML and
- # DDL operations, we need to rely on exception to figure
- # that out at the moment.
- try:
- if records == -1:
- res = cur.fetchall()
- else:
- res = cur.fetchmany(records)
- for row in res:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
- except psycopg2.ProgrammingError as e:
- result = None
- else:
- # User performed operation which dose not produce record/s as
- # result.
- # for eg. DDL operations.
- return True, None
-
- return True, result
-
- def connected(self):
- if self.conn:
- if not self.conn.closed:
- return True
- self.conn = None
- return False
-
- def reset(self):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- pg_conn = None
- mgr = self.manager
-
- password = getattr(mgr, 'password', None)
-
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=self.db,
- user=mgr.user,
- password=password,
- passfile=get_complete_file_path(mgr.passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- except psycopg2.Error as e:
- msg = e.pgerror if e.pgerror else e.message \
- if e.message else e.diag.message_detail \
- if e.diag.message_detail else str(e)
-
- current_app.logger.error(
- gettext(
- """
-Failed to reset the connection to the server due to following error:
-{0}"""
- ).Format(msg)
- )
- return False, msg
-
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.__backend_pid = pg_conn.get_backend_pid()
-
- return True, None
-
- def transaction_status(self):
- if self.conn:
- return self.conn.get_transaction_status()
- return None
-
- def ping(self):
- return self.execute_scalar('SELECT 1')
-
- def _release(self):
- if self.wasConnected:
- if self.conn:
- self.conn.close()
- self.conn = None
- self.password = None
- self.wasConnected = False
-
- def _wait(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method in a infinite loop till poll
- returns psycopg2.extensions.POLL_OK. This is a blocking
- call.
-
- Args:
- conn: connection object
- """
-
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- break
- elif state == psycopg2.extensions.POLL_WRITE:
- select.select([], [conn.fileno()], [])
- elif state == psycopg2.extensions.POLL_READ:
- select.select([conn.fileno()], [], [])
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait function" % state)
-
- def _wait_timeout(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method and return the status. If state is
- psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
- function will wait for the given timeout.This is not a blocking call.
-
- Args:
- conn: connection object
- """
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- return self.ASYNC_OK
- elif state == psycopg2.extensions.POLL_WRITE:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([], [conn.fileno()], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_WRITE_TIMEOUT
- elif state == psycopg2.extensions.POLL_READ:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([conn.fileno()], [], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_READ_TIMEOUT
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait_timeout function" % state
- )
-
- def poll(self, formatted_exception_msg=False, no_result=False):
- """
- This function is a wrapper around connection's poll function.
- It internally uses the _wait_timeout method to poll the
- result on the connection object. In case of success it
- returns the result of the query.
-
- Args:
- formatted_exception_msg: if True then function return the formatted
- exception message, otherwise error string.
- no_result: If True then only poll status will be returned.
- """
-
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Polling result for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- is_error = False
- try:
- status = self._wait_timeout(self.conn)
- except psycopg2.Error as pe:
- if self.conn.closed:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- is_error = True
-
- if self.conn.notices and self.__notices is not None:
- self.__notices.extend(self.conn.notices)
- self.conn.notices.clear()
-
- # We also need to fetch notices before we return from function in case
- # of any Exception, To avoid code duplication we will return after
- # fetching the notices in case of any Exception
- if is_error:
- return False, errmsg
-
- result = None
- self.row_count = 0
- self.column_info = None
-
- if status == self.ASYNC_OK:
-
- # if user has cancelled the transaction then changed the status
- if self.execution_aborted:
- status = self.ASYNC_EXECUTION_ABORTED
- self.execution_aborted = False
- return status, result
-
- # Fetch the column information
- if cur.description is not None:
- self.column_info = [
- desc.to_dict() for desc in cur.ordered_description()
- ]
-
- pos = 0
- for col in self.column_info:
- col['pos'] = pos
- pos += 1
-
- self.row_count = cur.rowcount
- if not no_result:
- if cur.rowcount > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML
- # and DDL operations, we need to rely on exception to
- # figure that out at the moment.
- try:
- for row in cur:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
-
- except psycopg2.ProgrammingError:
- result = None
-
- return status, result
-
- def status_message(self):
- """
- This function will return the status message returned by the last
- command executed on the server.
- """
- cur = self.__async_cursor
- if not cur:
- return gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Status message for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- return cur.statusmessage
-
- def rows_affected(self):
- """
- This function will return the no of rows affected by the last command
- executed on the server.
- """
-
- return self.row_count
-
- def get_column_info(self):
- """
- This function will returns list of columns for last async sql command
- executed on the server.
- """
-
- return self.column_info
-
- def cancel_transaction(self, conn_id, did=None):
- """
- This function is used to cancel the running transaction
- of the given connection id and database id using
- PostgreSQL's pg_cancel_backend.
-
- Args:
- conn_id: Connection id
- did: Database id (optional)
- """
- cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
- query = """SELECT pg_cancel_backend({0});""".format(
- cancel_conn.__backend_pid)
-
- status = True
- msg = ''
-
- # if backend pid is same then create a new connection
- # to cancel the query and release it.
- if cancel_conn.__backend_pid == self.__backend_pid:
- password = getattr(self.manager, 'password', None)
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=self.manager.host,
- hostaddr=self.manager.hostaddr,
- port=self.manager.port,
- database=self.db,
- user=self.manager.user,
- password=password,
- passfile=get_complete_file_path(self.manager.passfile),
- sslmode=self.manager.ssl_mode,
- sslcert=get_complete_file_path(self.manager.sslcert),
- sslkey=get_complete_file_path(self.manager.sslkey),
- sslrootcert=get_complete_file_path(
- self.manager.sslrootcert
- ),
- sslcrl=get_complete_file_path(self.manager.sslcrl),
- sslcompression=True if self.manager.sslcompression
- else False
- )
-
- # Get the cursor and run the query
- cur = pg_conn.cursor()
- cur.execute(query)
-
- # Close the connection
- pg_conn.close()
- pg_conn = None
-
- except psycopg2.Error as e:
- status = False
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- return status, msg
- else:
- if self.connected():
- status, msg = self.execute_void(query)
-
- if status:
- cancel_conn.execution_aborted = True
- else:
- status = False
- msg = gettext("Not connected to the database server.")
-
- return status, msg
-
- def messages(self):
- """
- Returns the list of the messages/notices send from the database server.
- """
- resp = []
- while self.__notices:
- resp.append(self.__notices.pop(0))
- return resp
-
- def decode_to_utf8(self, value):
- """
- This method will decode values to utf-8
- Args:
- value: String to be decode
-
- Returns:
- Decoded string
- """
- is_error = False
- if hasattr(str, 'decode'):
- try:
- value = value.decode('utf-8')
- except UnicodeDecodeError:
- # Let's try with python's preferred encoding
- # On Windows lc_messages mostly has environment dependent
- # encoding like 'French_France.1252'
- try:
- import locale
- pref_encoding = locale.getpreferredencoding()
- value = value.decode(pref_encoding)\
- .encode('utf-8')\
- .decode('utf-8')
- except Exception:
- is_error = True
- except Exception:
- is_error = True
-
- # If still not able to decode then
- if is_error:
- value = value.decode('ascii', 'ignore')
-
- return value
-
- def _formatted_exception_msg(self, exception_obj, formatted_msg):
- """
- This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- Args:
- exception_obj: exception object
- formatted_msg: if True then function return the formatted exception
- message
-
- """
- if exception_obj.pgerror:
- errmsg = exception_obj.pgerror
- elif exception_obj.diag.message_detail:
- errmsg = exception_obj.diag.message_detail
- else:
- errmsg = str(exception_obj)
- # errmsg might contains encoded value, lets decode it
- errmsg = self.decode_to_utf8(errmsg)
-
- # if formatted_msg is false then return from the function
- if not formatted_msg:
- return errmsg
-
- # Do not append if error starts with `ERROR:` as most pg related
- # error starts with `ERROR:`
- if not errmsg.startswith(u'ERROR:'):
- errmsg = u'ERROR: ' + errmsg + u'\n\n'
-
- if exception_obj.diag.severity is not None \
- and exception_obj.diag.message_primary is not None:
- ex_diag_message = u"{0}: {1}".format(
- exception_obj.diag.severity,
- self.decode_to_utf8(exception_obj.diag.message_primary)
- )
- # If both errors are different then only append it
- if errmsg and ex_diag_message and \
- ex_diag_message.strip().strip('\n').lower() not in \
- errmsg.strip().strip('\n').lower():
- errmsg += ex_diag_message
- elif exception_obj.diag.message_primary is not None:
- message_primary = self.decode_to_utf8(
- exception_obj.diag.message_primary
- )
- if message_primary.lower() not in errmsg.lower():
- errmsg += message_primary
-
- if exception_obj.diag.sqlstate is not None:
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('SQL state: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
-
- if exception_obj.diag.message_detail is not None:
- if 'Detail:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Detail: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.message_detail
- )
-
- if exception_obj.diag.message_hint is not None:
- if 'Hint:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Hint: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
-
- if exception_obj.diag.statement_position is not None:
- if 'Character:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Character: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.statement_position
- )
-
- if exception_obj.diag.context is not None:
- if 'Context:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Context: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.context)
-
- return errmsg
-
- #####
- # As per issue reported on pgsycopg2 github repository link is shared below
- # conn.closed is not reliable enough to identify the disconnection from the
- # database server for some unknown reasons.
- #
- # (https://github.com/psycopg/psycopg2/issues/263)
- #
- # In order to resolve the issue, sqlalchamey follows the below logic to
- # identify the disconnection. It relies on exception message to identify
- # the error.
- #
- # Reference (MIT license):
- # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
- #
- def is_disconnected(self, err):
- if not self.conn.closed:
- # checks based on strings. in the case that .closed
- # didn't cut it, fall back onto these.
- str_e = str(err).partition("\n")[0]
- for msg in [
- # these error messages from libpq: interfaces/libpq/fe-misc.c
- # and interfaces/libpq/fe-secure.c.
- 'terminating connection',
- 'closed the connection',
- 'connection not open',
- 'could not receive data from server',
- 'could not send data to server',
- # psycopg2 client errors, psycopg2/conenction.h,
- # psycopg2/cursor.h
- 'connection already closed',
- 'cursor already closed',
- # not sure where this path is originally from, it may
- # be obsolete. It really says "losed", not "closed".
- 'losed the connection unexpectedly',
- # these can occur in newer SSL
- 'connection has been closed unexpectedly',
- 'SSL SYSCALL error: Bad file descriptor',
- 'SSL SYSCALL error: EOF detected',
- ]:
- idx = str_e.find(msg)
- if idx >= 0 and '"' not in str_e[:idx]:
- return True
-
- return False
- return True
-
-
-class ServerManager(object):
- """
- class ServerManager
-
- This class contains the information about the given server.
- And, acts as connection manager for that particular session.
- """
-
- def __init__(self, server):
- self.connections = dict()
-
- self.update(server)
-
- def update(self, server):
- assert (server is not None)
- assert (isinstance(server, Server))
-
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.sid = server.id
- self.host = server.host
- self.hostaddr = server.hostaddr
- self.port = server.port
- self.db = server.maintenance_db
- self.did = None
- self.user = server.username
- self.password = server.password
- self.role = server.role
- self.ssl_mode = server.ssl_mode
- self.pinged = datetime.datetime.now()
- self.db_info = dict()
- self.server_types = None
- self.db_res = server.db_res
- self.passfile = server.passfile
- self.sslcert = server.sslcert
- self.sslkey = server.sslkey
- self.sslrootcert = server.sslrootcert
- self.sslcrl = server.sslcrl
- self.sslcompression = True if server.sslcompression else False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.update_session()
-
- self.connections = dict()
-
- def as_dict(self):
- """
- Returns a dictionary object representing the server manager.
- """
- if self.ver is None or len(self.connections) == 0:
- return None
-
- res = dict()
- res['sid'] = self.sid
- res['ver'] = self.ver
- res['sversion'] = self.sversion
- if hasattr(self, 'password') and self.password:
- # If running under PY2
- if hasattr(self.password, 'decode'):
- res['password'] = self.password.decode('utf-8')
- else:
- res['password'] = str(self.password)
- else:
- res['password'] = self.password
-
- connections = res['connections'] = dict()
-
- for conn_id in self.connections:
- conn = self.connections[conn_id].as_dict()
-
- if conn is not None:
- connections[conn_id] = conn
-
- return res
-
- def ServerVersion(self):
- return self.ver
-
- @property
- def version(self):
- return self.sversion
-
- def MajorVersion(self):
- if self.sversion is not None:
- return int(self.sversion / 10000)
- raise Exception("Information is not available.")
-
- def MinorVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) % 100)
- raise Exception("Information is not available.")
-
- def PatchVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) / 100)
- raise Exception("Information is not available.")
-
- def connection(
- self, database=None, conn_id=None, auto_reconnect=True, did=None,
- async=None, use_binary_placeholder=False, array_to_string=False
- ):
- if database is not None:
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if did is not None:
- if did in self.db_info:
- self.db_info[did]['datname'] = database
- else:
- if did is None:
- database = self.db
- elif did in self.db_info:
- database = self.db_info[did]['datname']
- else:
- maintenance_db_id = u'DB:{0}'.format(self.db)
- if maintenance_db_id in self.connections:
- conn = self.connections[maintenance_db_id]
- if conn.connected():
- status, res = conn.execute_dict(u"""
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.oid = {0}""".format(did))
-
- if status and len(res['rows']) > 0:
- for row in res['rows']:
- self.db_info[did] = row
- database = self.db_info[did]['datname']
-
- if did not in self.db_info:
- raise Exception(gettext(
- "Could not find the specified database."
- ))
-
- if database is None:
- raise ConnectionLost(self.sid, None, None)
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database))
-
- self.pinged = datetime.datetime.now()
-
- if my_id in self.connections:
- return self.connections[my_id]
- else:
- if async is None:
- async = 1 if conn_id is not None else 0
- else:
- async = 1 if async is True else 0
- self.connections[my_id] = Connection(
- self, my_id, database, auto_reconnect, async,
- use_binary_placeholder=use_binary_placeholder,
- array_to_string=array_to_string
- )
-
- return self.connections[my_id]
-
- def _restore(self, data):
- """
- Helps restoring to reconnect the auto-connect connections smoothly on
- reload/restart of the app server..
- """
- # restore server version from flask session if flask server was
- # restarted. As we need server version to resolve sql template paths.
-
- self.ver = data.get('ver', None)
- self.sversion = data.get('sversion', None)
-
- if self.ver and not self.server_type:
- from pgadmin.browser.server_groups.servers.types import ServerType
- for st in ServerType.types():
- if st.instanceOf(self.ver):
- self.server_type = st.stype
- self.server_cls = st
- break
-
- # Hmm.. we will not honour this request, when I already have
- # connections
- if len(self.connections) != 0:
- return
-
- # We need to know about the existing server variant supports during
- # first connection for identifications.
- from pgadmin.browser.server_groups.servers.types import ServerType
- self.pinged = datetime.datetime.now()
- try:
- if 'password' in data and data['password']:
- data['password'] = data['password'].encode('utf-8')
- except Exception as e:
- current_app.logger.exception(e)
-
- connections = data['connections']
- for conn_id in connections:
- conn_info = connections[conn_id]
- conn = self.connections[conn_info['conn_id']] = Connection(
- self, conn_info['conn_id'], conn_info['database'],
- conn_info['auto_reconnect'], conn_info['async'],
- use_binary_placeholder=conn_info['use_binary_placeholder'],
- array_to_string=conn_info['array_to_string']
- )
-
- # only try to reconnect if connection was connected previously and
- # auto_reconnect is true.
- if conn_info['wasConnected'] and conn_info['auto_reconnect']:
- try:
- conn.connect(
- password=data['password'],
- server_types=ServerType.types()
- )
- # This will also update wasConnected flag in connection so
- # no need to update the flag manually.
- except Exception as e:
- current_app.logger.exception(e)
- self.connections.pop(conn_info['conn_id'])
-
- def release(self, database=None, conn_id=None, did=None):
- if did is not None:
- if did in self.db_info and 'datname' in self.db_info[did]:
- database = self.db_info[did]['datname']
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if database is None:
- return False
- else:
- return False
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database)) if database is not None else None
-
- if my_id is not None:
- if my_id in self.connections:
- self.connections[my_id]._release()
- del self.connections[my_id]
- if did is not None:
- del self.db_info[did]
-
- if len(self.connections) == 0:
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
- else:
- return False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.connections = dict()
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
-
- def _update_password(self, passwd):
- self.password = passwd
- for conn_id in self.connections:
- conn = self.connections[conn_id]
- if conn.conn is not None or conn.wasConnected is True:
- conn.password = passwd
-
- def update_session(self):
- managers = session['__pgsql_server_managers'] \
- if '__pgsql_server_managers' in session else dict()
- updated_mgr = self.as_dict()
-
- if not updated_mgr:
- if self.sid in managers:
- managers.pop(self.sid)
- else:
- managers[self.sid] = updated_mgr
- session['__pgsql_server_managers'] = managers
- session.force_write = True
-
- def utility(self, operation):
- """
- utility(operation)
-
- Returns: name of the utility which used for the operation
- """
- if self.server_cls is not None:
- return self.server_cls.utility(operation, self.sversion)
-
- return None
-
- def export_password_env(self, env):
- if self.password:
- password = decrypt(
- self.password, current_user.password
- ).decode()
- os.environ[str(env)] = password
+from ..abstract import BaseDriver
+from .connection import Connection
+from .server_manager import ServerManager
class Driver(BaseDriver):
@@ -2164,8 +202,9 @@ class Driver(BaseDriver):
continue
if curr_time - sess_mgr['pinged'] >= session_idle_timeout:
- for mgr in [m for m in sess_mgr if isinstance(m,
- ServerManager)]:
+ for mgr in [
+ m for m in sess_mgr if isinstance(m, ServerManager)
+ ]:
mgr.release()
@staticmethod
diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py
new file mode 100644
index 0000000..d4c9573
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/connection.py
@@ -0,0 +1,1682 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of Connection.
+It is a wrapper around the actual psycopg2 driver, and connection
+object.
+"""
+
+import random
+import select
+import sys
+from collections import deque
+import simplejson as json
+import psycopg2
+from flask import g, current_app
+from flask_babel import gettext
+from flask_security import current_user
+from pgadmin.utils.crypto import decrypt
+from psycopg2.extensions import adapt, encodings
+
+import config
+from pgadmin.model import Server, User
+from pgadmin.utils.exception import ConnectionLost
+from pgadmin.utils import get_complete_file_path
+from ..abstract import BaseDriver, BaseConnection
+from .cursor import DictCursor
+from .typecast import register_global_typecasters, \
+ register_string_typecasters, register_binary_typecasters, \
+ register_array_to_string_typecasters, ALL_JSON_TYPES
+
+
+if sys.version_info < (3,):
+ # Python2 in-built csv module do not handle unicode
+ # backports.csv module ported from PY3 csv module for unicode handling
+ from backports import csv
+ from StringIO import StringIO
+ IS_PY2 = True
+else:
+ from io import StringIO
+ import csv
+ IS_PY2 = False
+
+_ = gettext
+
+
+# Register global type caster which will be applicable to all connections.
+register_global_typecasters()
+
+
+class Connection(BaseConnection):
+ """
+ class Connection(object)
+
+ A wrapper class, which wraps the psycopg2 connection object, and
+ delegate the execution to the actual connection object, when required.
+
+ Methods:
+ -------
+ * connect(**kwargs)
+ - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
+ driver
+
+ * execute_scalar(query, params, formatted_exception_msg)
+ - Execute the given query and returns single datum result
+
+ * execute_async(query, params, formatted_exception_msg)
+ - Execute the given query asynchronously and returns result.
+
+ * execute_void(query, params, formatted_exception_msg)
+ - Execute the given query with no result.
+
+ * execute_2darray(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as a 2 dimensional
+ array.
+
+ * execute_dict(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as an array of dict
+ (column name -> value) format.
+
+ * connected()
+ - Get the status of the connection.
+ Returns True if connected, otherwise False.
+
+ * reset()
+ - Reconnect the database server (if possible)
+
+ * transaction_status()
+ - Transaction Status
+
+ * ping()
+ - Ping the server.
+
+ * _release()
+ - Release the connection object of psycopg2
+
+ * _reconnect()
+ - Attempt to reconnect to the database
+
+ * _wait(conn)
+ - This method is used to wait for asynchronous connection. This is a
+ blocking call.
+
+ * _wait_timeout(conn)
+ - This method is used to wait for asynchronous connection with timeout.
+ This is a non blocking call.
+
+ * poll(formatted_exception_msg)
+ - This method is used to poll the data of query running on asynchronous
+ connection.
+
+ * status_message()
+ - Returns the status message returned by the last command executed on
+ the server.
+
+ * rows_affected()
+ - Returns the no of rows affected by the last command executed on
+ the server.
+
+ * cancel_transaction(conn_id, did=None)
+ - This method is used to cancel the transaction for the
+ specified connection id and database id.
+
+ * messages()
+ - Returns the list of messages/notices sends from the PostgreSQL database
+ server.
+
+ * _formatted_exception_msg(exception_obj, formatted_msg)
+ - This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ """
+
+ def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
+ use_binary_placeholder=False, array_to_string=False):
+ assert (manager is not None)
+ assert (conn_id is not None)
+
+ self.conn_id = conn_id
+ self.manager = manager
+ self.db = db if db is not None else manager.db
+ self.conn = None
+ self.auto_reconnect = auto_reconnect
+ self.async = async
+ self.__async_cursor = None
+ self.__async_query_id = None
+ self.__backend_pid = None
+ self.execution_aborted = False
+ self.row_count = 0
+ self.__notices = None
+ self.password = None
+ # This flag indicates the connection status (connected/disconnected).
+ self.wasConnected = False
+ # This flag indicates the connection reconnecting status.
+ self.reconnecting = False
+ self.use_binary_placeholder = use_binary_placeholder
+ self.array_to_string = array_to_string
+
+ super(Connection, self).__init__()
+
+ def as_dict(self):
+ """
+ Returns the dictionary object representing this object.
+ """
+ # In case, it cannot be auto reconnectable, or already been released,
+ # then we will return None.
+ if not self.auto_reconnect and not self.conn:
+ return None
+
+ res = dict()
+ res['conn_id'] = self.conn_id
+ res['database'] = self.db
+ res['async'] = self.async
+ res['wasConnected'] = self.wasConnected
+ res['auto_reconnect'] = self.auto_reconnect
+ res['use_binary_placeholder'] = self.use_binary_placeholder
+ res['array_to_string'] = self.array_to_string
+
+ return res
+
+ def __repr__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def __str__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def connect(self, **kwargs):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ else:
+ return True, None
+
+ pg_conn = None
+ password = None
+ passfile = None
+ mgr = self.manager
+
+ encpass = kwargs['password'] if 'password' in kwargs else None
+ passfile = kwargs['passfile'] if 'passfile' in kwargs else None
+
+ if encpass is None:
+ encpass = self.password or getattr(mgr, 'password', None)
+
+ # Reset the existing connection password
+ if self.reconnecting is not False:
+ self.password = None
+
+ if encpass:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ try:
+ password = decrypt(encpass, user.password)
+ # Handling of non ascii password (Python2)
+ if hasattr(str, 'decode'):
+ password = password.decode('utf-8').encode('utf-8')
+ # password is in bytes, for python3 we need it in string
+ elif isinstance(password, bytes):
+ password = password.decode()
+
+ except Exception as e:
+ current_app.logger.exception(e)
+ return False, \
+ _(
+ "Failed to decrypt the saved password.\nError: {0}"
+ ).format(str(e))
+
+ # If no password credential is found then connect request might
+ # come from Query tool, ViewData grid, debugger etc tools.
+ # we will check for pgpass file availability from connection manager
+ # if it's present then we will use it
+ if not password and not encpass and not passfile:
+ passfile = mgr.passfile if mgr.passfile else None
+
+ try:
+ if hasattr(str, 'decode'):
+ database = self.db.encode('utf-8')
+ user = mgr.user.encode('utf-8')
+ conn_id = self.conn_id.encode('utf-8')
+ else:
+ database = self.db
+ user = mgr.user
+ conn_id = self.conn_id
+
+ import os
+ os.environ['PGAPPNAME'] = '{0} - {1}'.format(
+ config.APP_NAME, conn_id)
+
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=database,
+ user=user,
+ password=password,
+ async=self.async,
+ passfile=get_complete_file_path(passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ # If connection is asynchronous then we will have to wait
+ # until the connection is ready to use.
+ if self.async == 1:
+ self._wait(pg_conn)
+
+ except psycopg2.Error as e:
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ current_app.logger.info(
+ u"Failed to connect to the database server(#{server_id}) for "
+ u"connection ({conn_id}) with error message as below"
+ u":{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
+ )
+ )
+ return False, msg
+
+ # Overwrite connection notice attr to support
+ # more than 50 notices at a time
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+
+ self.conn = pg_conn
+ self.wasConnected = True
+ try:
+ status, msg = self._initialize(conn_id, **kwargs)
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.conn = None
+ if not self.reconnecting:
+ self.wasConnected = False
+ raise e
+
+ if status:
+ mgr._update_password(encpass)
+ else:
+ if not self.reconnecting:
+ self.wasConnected = False
+
+ return status, msg
+
+ def _initialize(self, conn_id, **kwargs):
+ self.execution_aborted = False
+ self.__backend_pid = self.conn.get_backend_pid()
+
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ status, cur = self.__cursor()
+ formatted_exception_msg = self._formatted_exception_msg
+ mgr = self.manager
+
+ def _execute(cur, query, params=None):
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ return formatted_exception_msg(pe, False)
+ return None
+
+ # autocommit flag does not work with asynchronous connections.
+ # By default asynchronous connection runs in autocommit mode.
+ if self.async == 0:
+ if 'autocommit' in kwargs and kwargs['autocommit'] is False:
+ self.conn.autocommit = False
+ else:
+ self.conn.autocommit = True
+
+ register_string_typecasters(self.conn)
+
+ if self.array_to_string:
+ register_array_to_string_typecasters(self.conn)
+
+ # Register type casters for binary data only after registering array to
+ # string type casters.
+ if self.use_binary_placeholder:
+ register_binary_typecasters(self.conn)
+
+ status = _execute(cur, "SET DateStyle=ISO;"
+ "SET client_min_messages=notice;"
+ "SET bytea_output=escape;"
+ "SET client_encoding='UNICODE';")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+
+ return False, status
+
+ if mgr.role:
+ status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ current_app.logger.error(
+ "Connect to the database server (#{server_id}) for "
+ "connection ({conn_id}), but - failed to setup the role "
+ "with error message as below:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status
+ )
+ )
+ return False, \
+ _(
+ "Failed to setup the role with error message:\n{0}"
+ ).format(status)
+
+ if mgr.ver is None:
+ status = _execute(cur, "SELECT version()")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ self.wasConnected = False
+ current_app.logger.error(
+ "Failed to fetch the version information on the "
+ "established connection to the database server "
+ "(#{server_id}) for '{conn_id}' with below error "
+ "message:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status)
+ )
+ return False, status
+
+ if cur.rowcount > 0:
+ row = cur.fetchmany(1)[0]
+ mgr.ver = row['version']
+ mgr.sversion = self.conn.server_version
+
+ status = _execute(cur, """
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.datname = current_database()""")
+
+ if status is None:
+ mgr.db_info = mgr.db_info or dict()
+ if cur.rowcount > 0:
+ res = cur.fetchmany(1)[0]
+ mgr.db_info[res['did']] = res.copy()
+
+ # We do not have database oid for the maintenance database.
+ if len(mgr.db_info) == 1:
+ mgr.did = res['did']
+
+ status = _execute(cur, """
+SELECT
+ oid as id, rolname as name, rolsuper as is_superuser,
+ rolcreaterole as can_create_role, rolcreatedb as can_create_db
+FROM
+ pg_catalog.pg_roles
+WHERE
+ rolname = current_user""")
+
+ if status is None:
+ mgr.user_info = dict()
+ if cur.rowcount > 0:
+ mgr.user_info = cur.fetchmany(1)[0]
+
+ if 'password' in kwargs:
+ mgr.password = kwargs['password']
+
+ server_types = None
+ if 'server_types' in kwargs and isinstance(
+ kwargs['server_types'], list):
+ server_types = mgr.server_types = kwargs['server_types']
+
+ if server_types is None:
+ from pgadmin.browser.server_groups.servers.types import ServerType
+ server_types = ServerType.types()
+
+ for st in server_types:
+ if st.instanceOf(mgr.ver):
+ mgr.server_type = st.stype
+ mgr.server_cls = st
+ break
+
+ mgr.update_session()
+
+ return True, None
+
+ def __cursor(self, server_cursor=False):
+ if self.wasConnected is False:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ cur = getattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ if self.connected() and cur and not cur.closed:
+ if not server_cursor or (server_cursor and cur.name):
+ return True, cur
+
+ if not self.connected():
+ errmsg = ""
+
+ current_app.logger.warning(
+ "Connection to database server (#{server_id}) for the "
+ "connection - '{conn_id}' has been lost.".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+
+ if self.auto_reconnect and not self.reconnecting:
+ self.__attempt_execution_reconnect(None)
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ try:
+ if server_cursor:
+ # Providing name to cursor will create server side cursor.
+ cursor_name = "CURSOR:{0}".format(self.conn_id)
+ cur = self.conn.cursor(
+ name=cursor_name, cursor_factory=DictCursor
+ )
+ else:
+ cur = self.conn.cursor(cursor_factory=DictCursor)
+ except psycopg2.Error as pe:
+ current_app.logger.exception(pe)
+ errmsg = gettext(
+ "Failed to create cursor for psycopg2 connection with error "
+ "message for the server#{1}:{2}:\n{0}"
+ ).format(
+ str(pe), self.manager.sid, self.db
+ )
+
+ current_app.logger.error(errmsg)
+ if self.conn.closed:
+ self.conn = None
+ if self.auto_reconnect and not self.reconnecting:
+ current_app.logger.info(
+ gettext(
+ "Attempting to reconnect to the database server "
+ "(#{server_id}) for the connection - '{conn_id}'."
+ ).format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ return self.__attempt_execution_reconnect(
+ self.__cursor, server_cursor
+ )
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:'
+ else self.conn_id[5:]
+ )
+
+ setattr(
+ g, "{0}#{1}".format(
+ self.manager.sid, self.conn_id.encode('utf-8')
+ ), cur
+ )
+
+ return True, cur
+
+ def __internal_blocking_execute(self, cur, query, params):
+ """
+ This function executes the query using cursor's execute function,
+ but in case of asynchronous connection we need to wait for the
+ transaction to be completed. If self.async is 1 then it is a
+ blocking call.
+
+ Args:
+ cur: Cursor object
+ query: SQL query to run.
+ params: Extra parameters
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ cur.execute(query, params)
+ if self.async == 1:
+ self._wait(cur.connection)
+
+ def execute_on_server_as_csv(self,
+ query, params=None,
+ formatted_exception_msg=False,
+ records=2000):
+ """
+ To fetch query result and generate CSV output
+
+ Args:
+ query: SQL
+ params: Additional parameters
+ formatted_exception_msg: For exception
+ records: Number of initial records
+ Returns:
+ Generator response
+ """
+ status, cur = self.__cursor(server_cursor=True)
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ if IS_PY2 and type(query) == unicode:
+ query = query.encode('utf-8')
+
+ current_app.logger.log(
+ 25,
+ u"Execute (with server cursor) for server #{server_id} - "
+ u"{conn_id} (Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8') if
+ sys.version_info < (3,) else query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"failed to execute query ((with server cursor) "
+ u"for the server #{server_id} - {conn_id} "
+ u"(query-id: {query_id}):\nerror message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ def handle_json_data(json_columns, results):
+ """
+ [ This is only for Python2.x]
+ This function will be useful to handle json data types.
+ We will dump json data as proper json instead of unicode values
+
+ Args:
+ json_columns: Columns which contains json data
+ results: Query result
+
+ Returns:
+ results
+ """
+ # Only if Python2 and there are columns with JSON type
+ if IS_PY2 and len(json_columns) > 0:
+ temp_results = []
+ for row in results:
+ res = dict()
+ for k, v in row.items():
+ if k in json_columns:
+ res[k] = json.dumps(v)
+ else:
+ res[k] = v
+ temp_results.append(res)
+ results = temp_results
+ return results
+
+ def convert_keys_to_unicode(results, conn_encoding):
+ """
+ [ This is only for Python2.x]
+ We need to convert all keys to unicode as psycopg2
+ sends them as string
+
+ Args:
+ res: Query result set from psycopg2
+ conn_encoding: Connection encoding
+
+ Returns:
+ Result set (With all the keys converted to unicode)
+ """
+ new_results = []
+ for row in results:
+ new_results.append(
+ dict([(k.decode(conn_encoding), v)
+ for k, v in row.items()])
+ )
+ return new_results
+
+ def gen(quote='strings', quote_char="'", field_separator=','):
+
+ results = cur.fetchmany(records)
+ if not results:
+ if not cur.closed:
+ cur.close()
+ yield gettext('The query executed did not return any data.')
+ return
+
+ header = []
+ json_columns = []
+ conn_encoding = cur.connection.encoding
+
+ for c in cur.ordered_description():
+ # This is to handle the case in which column name is non-ascii
+ column_name = c.to_dict()['name']
+ if IS_PY2:
+ column_name = column_name.decode(conn_encoding)
+ header.append(column_name)
+ if c.to_dict()['type_code'] in ALL_JSON_TYPES:
+ json_columns.append(column_name)
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ res_io = StringIO()
+
+ if quote == 'strings':
+ quote = csv.QUOTE_NONNUMERIC
+ elif quote == 'all':
+ quote = csv.QUOTE_ALL
+ else:
+ quote = csv.QUOTE_NONE
+
+ if hasattr(str, 'decode'):
+ # Decode the field_separator
+ try:
+ field_separator = field_separator.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ # Decode the quote_char
+ try:
+ quote_char = quote_char.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ csv_writer.writeheader()
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+
+ yield res_io.getvalue()
+
+ while True:
+ results = cur.fetchmany(records)
+
+ if not results:
+ if not cur.closed:
+ cur.close()
+ break
+ res_io = StringIO()
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+ yield res_io.getvalue()
+
+ return True, gen
+
+ def execute_scalar(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_scalar) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ res = cur.fetchone()
+ if len(res) > 0:
+ return True, res[0]
+
+ return True, None
+
+ def execute_async(self, query, params=None, formatted_exception_msg=True):
+ """
+ This function executes the given query asynchronously and returns
+ result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ self.__async_cursor = None
+ status, cur = self.__cursor()
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__notices = []
+ self.execution_aborted = False
+ cur.execute(query, params)
+ res = self._wait_timeout(cur.connection)
+ except psycopg2.Error as pe:
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_async) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+
+ if self.is_disconnected(pe):
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ return False, errmsg
+
+ self.__async_cursor = cur
+ self.__async_query_id = query_id
+
+ return True, res
+
+ def execute_void(self, query, params=None, formatted_exception_msg=False):
+ """
+ This function executes the given query with no result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_void, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_void) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+
+ return True, None
+
+ def __attempt_execution_reconnect(self, fn, *args, **kwargs):
+ self.reconnecting = True
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+ try:
+ status, res = self.connect()
+ if status:
+ if fn:
+ status, res = fn(*args, **kwargs)
+ self.reconnecting = False
+ return status, res
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.reconnecting = False
+
+ current_app.warning(
+ "Failed to reconnect the database server "
+ "(#{server_id})".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ self.reconnecting = False
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ def execute_2darray(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (2darray) for server #{server_id} - {conn_id} "
+ u"(Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and \
+ not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_2darray, query, params,
+ formatted_exception_msg
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_2darray) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(row)
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def execute_dict(self, query, params=None, formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_dict) for the server "
+ u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query_id=query_id,
+ errmsg=errmsg
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(dict(row))
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def async_fetchmany_2darray(self, records=2000,
+ formatted_exception_msg=False):
+ """
+ User should poll and check if status is ASYNC_OK before calling this
+ function
+ Args:
+ records: no of records to fetch. use -1 to fetchall.
+ formatted_exception_msg:
+
+ Returns:
+
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ if self.conn.isexecuting():
+ return False, gettext(
+ "Asynchronous query execution/operation underway."
+ )
+
+ if self.row_count > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML and
+ # DDL operations, we need to rely on exception to figure
+ # that out at the moment.
+ try:
+ if records == -1:
+ res = cur.fetchall()
+ else:
+ res = cur.fetchmany(records)
+ for row in res:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+ except psycopg2.ProgrammingError as e:
+ result = None
+ else:
+ # User performed operation which dose not produce record/s as
+ # result.
+ # for eg. DDL operations.
+ return True, None
+
+ return True, result
+
+ def connected(self):
+ if self.conn:
+ if not self.conn.closed:
+ return True
+ self.conn = None
+ return False
+
+ def reset(self):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ pg_conn = None
+ mgr = self.manager
+
+ password = getattr(mgr, 'password', None)
+
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=self.db,
+ user=mgr.user,
+ password=password,
+ passfile=get_complete_file_path(mgr.passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ except psycopg2.Error as e:
+ msg = e.pgerror if e.pgerror else e.message \
+ if e.message else e.diag.message_detail \
+ if e.diag.message_detail else str(e)
+
+ current_app.logger.error(
+ gettext(
+ """
+Failed to reset the connection to the server due to following error:
+{0}"""
+ ).Format(msg)
+ )
+ return False, msg
+
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+ self.conn = pg_conn
+ self.__backend_pid = pg_conn.get_backend_pid()
+
+ return True, None
+
+ def transaction_status(self):
+ if self.conn:
+ return self.conn.get_transaction_status()
+ return None
+
+ def ping(self):
+ return self.execute_scalar('SELECT 1')
+
+ def _release(self):
+ if self.wasConnected:
+ if self.conn:
+ self.conn.close()
+ self.conn = None
+ self.password = None
+ self.wasConnected = False
+
+ def _wait(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method in a infinite loop till poll
+ returns psycopg2.extensions.POLL_OK. This is a blocking
+ call.
+
+ Args:
+ conn: connection object
+ """
+
+ while 1:
+ state = conn.poll()
+ if state == psycopg2.extensions.POLL_OK:
+ break
+ elif state == psycopg2.extensions.POLL_WRITE:
+ select.select([], [conn.fileno()], [])
+ elif state == psycopg2.extensions.POLL_READ:
+ select.select([conn.fileno()], [], [])
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait function" % state)
+
+ def _wait_timeout(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method and return the status. If state is
+ psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
+ function will wait for the given timeout.This is not a blocking call.
+
+ Args:
+ conn: connection object
+ time: wait time
+ """
+
+ while 1:
+ state = conn.poll()
+
+ if state == psycopg2.extensions.POLL_OK:
+ return self.ASYNC_OK
+ elif state == psycopg2.extensions.POLL_WRITE:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [], [conn.fileno()], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_WRITE_TIMEOUT
+ elif state == psycopg2.extensions.POLL_READ:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [conn.fileno()], [], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_READ_TIMEOUT
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait_timeout function" % state
+ )
+
+ def poll(self, formatted_exception_msg=False, no_result=False):
+ """
+ This function is a wrapper around connection's poll function.
+ It internally uses the _wait_timeout method to poll the
+ result on the connection object. In case of success it
+ returns the result of the query.
+
+ Args:
+ formatted_exception_msg: if True then function return the formatted
+ exception message, otherwise error string.
+ no_result: If True then only poll status will be returned.
+ """
+
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Polling result for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ is_error = False
+ try:
+ status = self._wait_timeout(self.conn)
+ except psycopg2.Error as pe:
+ if self.conn.closed:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ is_error = True
+
+ if self.conn.notices and self.__notices is not None:
+ self.__notices.extend(self.conn.notices)
+ self.conn.notices.clear()
+
+ # We also need to fetch notices before we return from function in case
+ # of any Exception, To avoid code duplication we will return after
+ # fetching the notices in case of any Exception
+ if is_error:
+ return False, errmsg
+
+ result = None
+ self.row_count = 0
+ self.column_info = None
+
+ if status == self.ASYNC_OK:
+
+ # if user has cancelled the transaction then changed the status
+ if self.execution_aborted:
+ status = self.ASYNC_EXECUTION_ABORTED
+ self.execution_aborted = False
+ return status, result
+
+ # Fetch the column information
+ if cur.description is not None:
+ self.column_info = [
+ desc.to_dict() for desc in cur.ordered_description()
+ ]
+
+ pos = 0
+ for col in self.column_info:
+ col['pos'] = pos
+ pos += 1
+
+ self.row_count = cur.rowcount
+ if not no_result:
+ if cur.rowcount > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML
+ # and DDL operations, we need to rely on exception to
+ # figure that out at the moment.
+ try:
+ for row in cur:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+
+ except psycopg2.ProgrammingError:
+ result = None
+
+ return status, result
+
+ def status_message(self):
+ """
+ This function will return the status message returned by the last
+ command executed on the server.
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Status message for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ return cur.statusmessage
+
+ def rows_affected(self):
+ """
+ This function will return the no of rows affected by the last command
+ executed on the server.
+ """
+
+ return self.row_count
+
+ def get_column_info(self):
+ """
+ This function will returns list of columns for last async sql command
+ executed on the server.
+ """
+
+ return self.column_info
+
+ def cancel_transaction(self, conn_id, did=None):
+ """
+ This function is used to cancel the running transaction
+ of the given connection id and database id using
+ PostgreSQL's pg_cancel_backend.
+
+ Args:
+ conn_id: Connection id
+ did: Database id (optional)
+ """
+ cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
+ query = """SELECT pg_cancel_backend({0});""".format(
+ cancel_conn.__backend_pid)
+
+ status = True
+ msg = ''
+
+ # if backend pid is same then create a new connection
+ # to cancel the query and release it.
+ if cancel_conn.__backend_pid == self.__backend_pid:
+ password = getattr(self.manager, 'password', None)
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=self.manager.host,
+ hostaddr=self.manager.hostaddr,
+ port=self.manager.port,
+ database=self.db,
+ user=self.manager.user,
+ password=password,
+ passfile=get_complete_file_path(self.manager.passfile),
+ sslmode=self.manager.ssl_mode,
+ sslcert=get_complete_file_path(self.manager.sslcert),
+ sslkey=get_complete_file_path(self.manager.sslkey),
+ sslrootcert=get_complete_file_path(
+ self.manager.sslrootcert
+ ),
+ sslcrl=get_complete_file_path(self.manager.sslcrl),
+ sslcompression=True if self.manager.sslcompression
+ else False,
+ service=self.manager.service
+ )
+
+ # Get the cursor and run the query
+ cur = pg_conn.cursor()
+ cur.execute(query)
+
+ # Close the connection
+ pg_conn.close()
+ pg_conn = None
+
+ except psycopg2.Error as e:
+ status = False
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ return status, msg
+ else:
+ if self.connected():
+ status, msg = self.execute_void(query)
+
+ if status:
+ cancel_conn.execution_aborted = True
+ else:
+ status = False
+ msg = gettext("Not connected to the database server.")
+
+ return status, msg
+
+ def messages(self):
+ """
+ Returns the list of the messages/notices send from the database server.
+ """
+ resp = []
+ while self.__notices:
+ resp.append(self.__notices.pop(0))
+ return resp
+
+ def decode_to_utf8(self, value):
+ """
+ This method will decode values to utf-8
+ Args:
+ value: String to be decode
+
+ Returns:
+ Decoded string
+ """
+ is_error = False
+ if hasattr(str, 'decode'):
+ try:
+ value = value.decode('utf-8')
+ except UnicodeDecodeError:
+ # Let's try with python's preferred encoding
+ # On Windows lc_messages mostly has environment dependent
+ # encoding like 'French_France.1252'
+ try:
+ import locale
+ pref_encoding = locale.getpreferredencoding()
+ value = value.decode(pref_encoding)\
+ .encode('utf-8')\
+ .decode('utf-8')
+ except Exception:
+ is_error = True
+ except Exception:
+ is_error = True
+
+ # If still not able to decode then
+ if is_error:
+ value = value.decode('ascii', 'ignore')
+
+ return value
+
+ def _formatted_exception_msg(self, exception_obj, formatted_msg):
+ """
+ This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ Args:
+ exception_obj: exception object
+ formatted_msg: if True then function return the formatted exception
+ message
+
+ """
+ if exception_obj.pgerror:
+ errmsg = exception_obj.pgerror
+ elif exception_obj.diag.message_detail:
+ errmsg = exception_obj.diag.message_detail
+ else:
+ errmsg = str(exception_obj)
+ # errmsg might contains encoded value, lets decode it
+ errmsg = self.decode_to_utf8(errmsg)
+
+ # if formatted_msg is false then return from the function
+ if not formatted_msg:
+ return errmsg
+
+ # Do not append if error starts with `ERROR:` as most pg related
+ # error starts with `ERROR:`
+ if not errmsg.startswith(u'ERROR:'):
+ errmsg = u'ERROR: ' + errmsg + u'\n\n'
+
+ if exception_obj.diag.severity is not None \
+ and exception_obj.diag.message_primary is not None:
+ ex_diag_message = u"{0}: {1}".format(
+ exception_obj.diag.severity,
+ self.decode_to_utf8(exception_obj.diag.message_primary)
+ )
+ # If both errors are different then only append it
+ if errmsg and ex_diag_message and \
+ ex_diag_message.strip().strip('\n').lower() not in \
+ errmsg.strip().strip('\n').lower():
+ errmsg += ex_diag_message
+ elif exception_obj.diag.message_primary is not None:
+ message_primary = self.decode_to_utf8(
+ exception_obj.diag.message_primary
+ )
+ if message_primary.lower() not in errmsg.lower():
+ errmsg += message_primary
+
+ if exception_obj.diag.sqlstate is not None:
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('SQL state: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
+
+ if exception_obj.diag.message_detail is not None:
+ if 'Detail:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Detail: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.message_detail
+ )
+
+ if exception_obj.diag.message_hint is not None:
+ if 'Hint:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Hint: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
+
+ if exception_obj.diag.statement_position is not None:
+ if 'Character:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Character: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.statement_position
+ )
+
+ if exception_obj.diag.context is not None:
+ if 'Context:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Context: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.context)
+
+ return errmsg
+
+ #####
+ # As per issue reported on pgsycopg2 github repository link is shared below
+ # conn.closed is not reliable enough to identify the disconnection from the
+ # database server for some unknown reasons.
+ #
+ # (https://github.com/psycopg/psycopg2/issues/263)
+ #
+ # In order to resolve the issue, sqlalchamey follows the below logic to
+ # identify the disconnection. It relies on exception message to identify
+ # the error.
+ #
+ # Reference (MIT license):
+ # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
+ #
+ def is_disconnected(self, err):
+ if not self.conn.closed:
+ # checks based on strings. in the case that .closed
+ # didn't cut it, fall back onto these.
+ str_e = str(err).partition("\n")[0]
+ for msg in [
+ # these error messages from libpq: interfaces/libpq/fe-misc.c
+ # and interfaces/libpq/fe-secure.c.
+ 'terminating connection',
+ 'closed the connection',
+ 'connection not open',
+ 'could not receive data from server',
+ 'could not send data to server',
+ # psycopg2 client errors, psycopg2/conenction.h,
+ # psycopg2/cursor.h
+ 'connection already closed',
+ 'cursor already closed',
+ # not sure where this path is originally from, it may
+ # be obsolete. It really says "losed", not "closed".
+ 'losed the connection unexpectedly',
+ # these can occur in newer SSL
+ 'connection has been closed unexpectedly',
+ 'SSL SYSCALL error: Bad file descriptor',
+ 'SSL SYSCALL error: EOF detected',
+ ]:
+ idx = str_e.find(msg)
+ if idx >= 0 and '"' not in str_e[:idx]:
+ return True
+
+ return False
+ return True
diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
new file mode 100644
index 0000000..2299e28
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -0,0 +1,333 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of ServerManager
+"""
+import os
+import datetime
+from flask import current_app, session
+from flask_security import current_user
+from flask_babel import gettext
+
+from pgadmin.utils.crypto import decrypt
+from .connection import Connection
+from pgadmin.model import Server
+
+
+class ServerManager(object):
+ """
+ class ServerManager
+
+ This class contains the information about the given server.
+ And, acts as connection manager for that particular session.
+ """
+
+ def __init__(self, server):
+ self.connections = dict()
+
+ self.update(server)
+
+ def update(self, server):
+ assert (server is not None)
+ assert (isinstance(server, Server))
+
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.sid = server.id
+ self.host = server.host
+ self.hostaddr = server.hostaddr
+ self.port = server.port
+ self.db = server.maintenance_db
+ self.did = None
+ self.user = server.username
+ self.password = server.password
+ self.role = server.role
+ self.ssl_mode = server.ssl_mode
+ self.pinged = datetime.datetime.now()
+ self.db_info = dict()
+ self.server_types = None
+ self.db_res = server.db_res
+ self.passfile = server.passfile
+ self.sslcert = server.sslcert
+ self.sslkey = server.sslkey
+ self.sslrootcert = server.sslrootcert
+ self.sslcrl = server.sslcrl
+ self.sslcompression = True if server.sslcompression else False
+ self.service = server.service
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.update_session()
+
+ self.connections = dict()
+
+ def as_dict(self):
+ """
+ Returns a dictionary object representing the server manager.
+ """
+ if self.ver is None or len(self.connections) == 0:
+ return None
+
+ res = dict()
+ res['sid'] = self.sid
+ res['ver'] = self.ver
+ res['sversion'] = self.sversion
+ if hasattr(self, 'password') and self.password:
+ # If running under PY2
+ if hasattr(self.password, 'decode'):
+ res['password'] = self.password.decode('utf-8')
+ else:
+ res['password'] = str(self.password)
+ else:
+ res['password'] = self.password
+
+ connections = res['connections'] = dict()
+
+ for conn_id in self.connections:
+ conn = self.connections[conn_id].as_dict()
+
+ if conn is not None:
+ connections[conn_id] = conn
+
+ return res
+
+ def ServerVersion(self):
+ return self.ver
+
+ @property
+ def version(self):
+ return self.sversion
+
+ def MajorVersion(self):
+ if self.sversion is not None:
+ return int(self.sversion / 10000)
+ raise Exception("Information is not available.")
+
+ def MinorVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) % 100)
+ raise Exception("Information is not available.")
+
+ def PatchVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) / 100)
+ raise Exception("Information is not available.")
+
+ def connection(
+ self, database=None, conn_id=None, auto_reconnect=True, did=None,
+ async=None, use_binary_placeholder=False, array_to_string=False
+ ):
+ if database is not None:
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if did is not None:
+ if did in self.db_info:
+ self.db_info[did]['datname'] = database
+ else:
+ if did is None:
+ database = self.db
+ elif did in self.db_info:
+ database = self.db_info[did]['datname']
+ else:
+ maintenance_db_id = u'DB:{0}'.format(self.db)
+ if maintenance_db_id in self.connections:
+ conn = self.connections[maintenance_db_id]
+ if conn.connected():
+ status, res = conn.execute_dict(u"""
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.oid = {0}""".format(did))
+
+ if status and len(res['rows']) > 0:
+ for row in res['rows']:
+ self.db_info[did] = row
+ database = self.db_info[did]['datname']
+
+ if did not in self.db_info:
+ raise Exception(gettext(
+ "Could not find the specified database."
+ ))
+
+ if database is None:
+ raise ConnectionLost(self.sid, None, None)
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database))
+
+ self.pinged = datetime.datetime.now()
+
+ if my_id in self.connections:
+ return self.connections[my_id]
+ else:
+ if async is None:
+ async = 1 if conn_id is not None else 0
+ else:
+ async = 1 if async is True else 0
+ self.connections[my_id] = Connection(
+ self, my_id, database, auto_reconnect, async,
+ use_binary_placeholder=use_binary_placeholder,
+ array_to_string=array_to_string
+ )
+
+ return self.connections[my_id]
+
+ def _restore(self, data):
+ """
+ Helps restoring to reconnect the auto-connect connections smoothly on
+ reload/restart of the app server..
+ """
+ # restore server version from flask session if flask server was
+ # restarted. As we need server version to resolve sql template paths.
+ from pgadmin.browser.server_groups.servers.types import ServerType
+
+ self.ver = data.get('ver', None)
+ self.sversion = data.get('sversion', None)
+
+ if self.ver and not self.server_type:
+ for st in ServerType.types():
+ if st.instanceOf(self.ver):
+ self.server_type = st.stype
+ self.server_cls = st
+ break
+
+ # Hmm.. we will not honour this request, when I already have
+ # connections
+ if len(self.connections) != 0:
+ return
+
+ # We need to know about the existing server variant supports during
+ # first connection for identifications.
+ self.pinged = datetime.datetime.now()
+ try:
+ if 'password' in data and data['password']:
+ data['password'] = data['password'].encode('utf-8')
+ except Exception as e:
+ current_app.logger.exception(e)
+
+ connections = data['connections']
+ for conn_id in connections:
+ conn_info = connections[conn_id]
+ conn = self.connections[conn_info['conn_id']] = Connection(
+ self, conn_info['conn_id'], conn_info['database'],
+ conn_info['auto_reconnect'], conn_info['async'],
+ use_binary_placeholder=conn_info['use_binary_placeholder'],
+ array_to_string=conn_info['array_to_string']
+ )
+
+ # only try to reconnect if connection was connected previously and
+ # auto_reconnect is true.
+ if conn_info['wasConnected'] and conn_info['auto_reconnect']:
+ try:
+ conn.connect(
+ password=data['password'],
+ server_types=ServerType.types()
+ )
+ # This will also update wasConnected flag in connection so
+ # no need to update the flag manually.
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.connections.pop(conn_info['conn_id'])
+
+ def release(self, database=None, conn_id=None, did=None):
+ if did is not None:
+ if did in self.db_info and 'datname' in self.db_info[did]:
+ database = self.db_info[did]['datname']
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if database is None:
+ return False
+ else:
+ return False
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database)) if database is not None else None
+
+ if my_id is not None:
+ if my_id in self.connections:
+ self.connections[my_id]._release()
+ del self.connections[my_id]
+ if did is not None:
+ del self.db_info[did]
+
+ if len(self.connections) == 0:
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+ else:
+ return False
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.connections = dict()
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+
+ def _update_password(self, passwd):
+ self.password = passwd
+ for conn_id in self.connections:
+ conn = self.connections[conn_id]
+ if conn.conn is not None or conn.wasConnected is True:
+ conn.password = passwd
+
+ def update_session(self):
+ managers = session['__pgsql_server_managers'] \
+ if '__pgsql_server_managers' in session else dict()
+ updated_mgr = self.as_dict()
+
+ if not updated_mgr:
+ if self.sid in managers:
+ managers.pop(self.sid)
+ else:
+ managers[self.sid] = updated_mgr
+ session['__pgsql_server_managers'] = managers
+ session.force_write = True
+
+ def utility(self, operation):
+ """
+ utility(operation)
+
+ Returns: name of the utility which used for the operation
+ """
+ if self.server_cls is not None:
+ return self.server_cls.utility(operation, self.sversion)
+
+ return None
+
+ def export_password_env(self, env):
+ if self.password:
+ password = decrypt(
+ self.password, current_user.password
+ ).decode()
+ os.environ[str(env)] = password
view thread (14+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected]
Subject: Re: [pgAdmin4][RM#3140] Add service parameter
In-Reply-To: <CAKKotZSrYHCypG0rfsD9DHCoE8-c+XAvBnU3vS8LMDfPqSrC1Q@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox