Skip to content

Commit c8eb6db

Browse files
committed
fix(db): Make using pg_service work again.
When I did the merge of schema support I broke pg_service.conf support by replacing get_database_name with db_schema_split. This commit fixes it. Also this commit returns the schema if one is specified in pg_service.conf. back_postgresql.py: Replace calls to db_schema_split() with get_database_schema_names() (new name for get_database_name()). Rename db_schema_split to _db_schema_split. It now returns a tuple (dbname, schema) rather than a list. It is used only by get_database_schema_names() which also returns tuples. get_database_schema_names() can also get schema info for the service (if present) as specified by pg_service.conf. Add get_database_user() to get the user from either RDBMS_USER or pg_service.conf. (User needed for creating schema, so not needed before schema patch. import re at the top of file and remove lower import. Remove some schema code from db_command as it's not needed. The database conection is done to either postgresql or template1 existing databases. This command never connects to the roundp specified db. test/test_postgresql.py: Reorganize top level imports, add import os. Replace import of db_schema_split with get_database_schema_names. Also replace calls to db_schema_split. Create new Opener for the service file. Set PGSERVICEFILE to point to test/pg_service.conf. Add three new classes to test Service: 1) using regular db 2) using schema within db 3) Unable to parse schema name from pg_service.conf. The last doesn't need a db. Number 1 and 2 reuse the tests in ROTest to verify db connectivity. test/pg_service.conf: three service connections for: db only, db and schema, and incorrectly specified schema test cases. doc/upgrading.txt: updated to current status. Included example schema definition in service file.
1 parent 5be4bee commit c8eb6db

File tree

4 files changed

+284
-40
lines changed

4 files changed

+284
-40
lines changed

doc/upgrading.txt

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,31 @@ keyword to ``roundup_database.roundup_schema`` will create
163163
and use the ``roundup_schema`` in the pre-created
164164
``roundup_database``.
165165

166-
Also there is a new configuration keyword in the rdbms section of
167-
``config.ini``. The ``service`` keyword allows you to define the
168-
service name for Postgres that will be looked up in the Postgres
169-
`Connection Service File`_. Setting service to `roundup` with the
170-
following in the service file::
166+
Also there is a new configuration keyword in the rdbms
167+
section of ``config.ini``. The ``service`` keyword allows
168+
you to define the service name for Postgres that will be
169+
looked up in the `Connection Service File`_. Any of the
170+
methods of specifying the file including by using the
171+
``PGSERVICEFILE`` environment variable are supported.
172+
173+
This is similar to the existing support for MySQL
174+
option/config files and groups.
175+
176+
If you use services, any settings for the same properties
177+
(user, name, password ...) that are in the tracker's
178+
``config.ini`` will override the service settings. So you
179+
want to leave the ``config.ini`` settings blank. E.G.::
180+
181+
[rdbms]
182+
name =
183+
host =
184+
port =
185+
user =
186+
password =
187+
service = roundup_roundup
188+
189+
Setting ``service`` to ``roundup_roundup`` with
190+
the following in the service file::
171191

172192
[roundup_roundup]
173193
host=127.0.0.1
@@ -176,13 +196,21 @@ following in the service file::
176196
password=roundup
177197
dbname=roundup
178198

179-
would use the roundup database with the specified credentials.
199+
would use the roundup database with the specified
200+
credentials. It is possible to define a service that
201+
connects to a specific schema using::
202+
203+
options=-c search_path=roundup_service_dev
204+
205+
Note that the first schema specified after ``search_path=``
206+
is created and populated. The schema name
207+
(``roundup_service_dev``) must be terminated by: a comma,
208+
whitespace or end of line.
180209

181-
It is possible to define a service that connects to a specific
182-
schema. However this will require a little fiddling to get things
183-
working. A future enhancement may make using a schema via this
184-
mechanism easier. See https://issues.roundup-tracker.org/issue2551299
185-
for details.
210+
You can use the command ``psql "service=db_service_name"``
211+
to verify the settings in the connection file. Inside of
212+
``psql`` you can verify the ``search_path`` using ``show
213+
search_path;``.
186214

187215
.. _`Connection Service File`: https://www.postgresql.org/docs/current/libpq-pgservice.html
188216

roundup/backends/back_postgresql.py

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,30 @@ def connection_dict(config, dbnamestr=None):
4848
del d['read_default_file']
4949
return d
5050

51-
def db_schema_split(database_name):
51+
def _db_schema_split(database_name):
5252
''' Split database_name into database and schema parts'''
5353
if '.' in database_name:
5454
return database_name.split ('.')
55-
return [database_name, '']
55+
return (database_name, '')
5656

5757
def db_create(config):
5858
"""Clear all database contents and drop database itself"""
59-
db_name, schema_name = db_schema_split(config.RDBMS_NAME)
59+
db_name, schema_name = get_database_schema_names(config)
6060
if not schema_name:
6161
command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'" % db_name
6262
if config.RDBMS_TEMPLATE:
6363
command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE
6464
logging.getLogger('roundup.hyperdb').info(command)
6565
db_command(config, command)
6666
else:
67-
command = "CREATE SCHEMA \"%s\" AUTHORIZATION \"%s\"" % (schema_name, config.RDBMS_USER)
67+
command = "CREATE SCHEMA \"%s\" AUTHORIZATION \"%s\"" % (
68+
schema_name, get_database_user_name(config))
6869
logging.getLogger('roundup.hyperdb').info(command)
6970
db_command(config, command, db_name)
7071

7172
def db_nuke(config):
7273
"""Drop the database (and all its contents) or the schema."""
73-
db_name, schema_name = db_schema_split(config.RDBMS_NAME)
74+
db_name, schema_name = get_database_schema_names(config)
7475
if not schema_name:
7576
command = 'DROP DATABASE "%s"'% db_name
7677
logging.getLogger('roundup.hyperdb').info(command)
@@ -82,28 +83,86 @@ def db_nuke(config):
8283
if os.path.exists(config.DATABASE):
8384
shutil.rmtree(config.DATABASE)
8485

85-
def get_database_name(config):
86-
'''Get database name using config.RDBMS_NAME or config.RDBMS_SERVICE.
86+
def get_database_schema_names(config):
87+
'''Get database and schema names using config.RDBMS_NAME or service
88+
defined by config.RDBMS_SERVICE.
89+
90+
If database specifed using RDBMS_SERVICE does not exist, the
91+
error message is parsed for the database name. This database
92+
can then be created by calling code. Parsing will fail if the
93+
error message changes. The alternative is to try to find and
94+
parse the .pg_service .ini style file on unix/windows. This is
95+
less palatable.
96+
97+
If the database specified using RDBMS_SERVICE exists, (e.g. we
98+
are doing a nuke operation), use
99+
psycopg.extensions.ConnectionInfo to get the dbname. Also parse
100+
the search_path options setting to get the schema. Only the
101+
first element of the search_path is returned. This requires
102+
psycopg2 > 2.8 from 2018.
103+
'''
87104

88-
If database specifed using RDBMS_SERVICE does not exist,
89-
the error message is parsed for the database name. This
90-
will fail if the error message changes. The alternative is
91-
to try to find and parse the .pg_service .ini style file on
92-
unix/windows. This is less palatable.
105+
if config.RDBMS_NAME:
106+
return _db_schema_split(config.RDBMS_NAME)
107+
108+
template1 = connection_dict(config)
109+
try:
110+
conn = psycopg2.connect(**template1)
111+
except psycopg2.OperationalError as message:
112+
# extract db name from error:
113+
# 'connection to server at "127.0.0.1", port 5432 failed: \
114+
# FATAL: database "rounduptest" does not exist\n'
115+
# ugh.
116+
#
117+
# Database name is any character sequence not including a " or
118+
# whitespace. Arguably both are allowed by:
119+
#
120+
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
121+
#
122+
# with suitable quoting but ... really.
123+
search = re.search(
124+
r'FATAL:\s+database\s+"([^"\s]*)"\s+does\s+not\s+exist',
125+
message.args[0])
126+
if search:
127+
dbname = search.groups()[0]
128+
# To use a schema, the db has to have been precreated.
129+
# So return '' for schema if database does not exist.
130+
return (dbname, '')
131+
132+
raise hyperdb.DatabaseError(
133+
"Unable to determine database from service: %s" % message)
134+
135+
dbname = psycopg2.extensions.ConnectionInfo(conn).dbname
136+
schema = ''
137+
options = psycopg2.extensions.ConnectionInfo(conn).options
138+
conn.close()
139+
140+
# Assume schema is first word in the search_path spec.
141+
# , (for multiple items in path) and whitespace (for another option)
142+
# end the schema name.
143+
m = re.search(r'search_path=([^,\s]*)', options)
144+
if m:
145+
schema = m.group(1)
146+
if not schema:
147+
raise ValueError('Unable to get schema for service: "%s" from options: "%s"' % (template1['service'], options))
148+
149+
return (dbname, schema)
150+
151+
def get_database_user_name(config):
152+
'''Get database username using config.RDBMS_USER or return
153+
user from connection created using config.RDBMS_SERVICE.
93154
94155
If the database specified using RDBMS_SERVICE does exist, (i.e. we
95156
are doing a nuke operation), use psycopg.extensions.ConnectionInfo
96-
to get the dbname. This requires psycopg2 > 2.8 from 2018.
157+
to get the user. This requires psycopg2 > 2.8 from 2018.
97158
'''
98-
99-
if config.RDBMS_NAME:
100-
return config.RDBMS_NAME
159+
if config.RDBMS_USER:
160+
return config.RDBMS_USER
101161

102162
template1 = connection_dict(config)
103163
try:
104164
conn = psycopg2.connect(**template1)
105165
except psycopg2.OperationalError as message:
106-
import re
107166
# extract db name from error:
108167
# 'connection to server at "127.0.0.1", port 5432 failed: \
109168
# FATAL: database "rounduptest" does not exist\n'
@@ -120,14 +179,17 @@ def get_database_name(config):
120179
message.args[0])
121180
if search:
122181
dbname = search.groups()[0]
123-
return dbname
182+
# To have a user, the db has to exist already.
183+
# so return '' for user.
184+
return ''
124185

125186
raise hyperdb.DatabaseError(
126187
"Unable to determine database from service: %s" % message)
127188

128-
dbname = psycopg2.extensions.ConnectionInfo(conn).dbname
189+
user = psycopg2.extensions.ConnectionInfo(conn).user
129190
conn.close()
130-
return dbname
191+
192+
return user
131193

132194
def db_command(config, command, database='postgres'):
133195
'''Perform some sort of database-level command. Retry 10 times if we
@@ -138,15 +200,13 @@ def db_command(config, command, database='postgres'):
138200
Compare to issue2550543.
139201
'''
140202
template1 = connection_dict(config, 'database')
141-
db_name, schema_name = db_schema_split(template1['database'])
142203
template1['database'] = database
143204

144205
try:
145206
conn = psycopg2.connect(**template1)
146207
except psycopg2.OperationalError as message:
147-
if not schema_name:
148-
if re.search(r'database ".+" does not exist', str(message)):
149-
return db_command(config, command, database='template1')
208+
if re.search(r'database ".+" does not exist', str(message)):
209+
return db_command(config, command, database='template1')
150210
raise hyperdb.DatabaseError(message)
151211

152212
conn.set_isolation_level(0)
@@ -186,7 +246,7 @@ def pg_command(cursor, command, args=()):
186246
def db_exists(config):
187247
"""Check if database or schema already exists"""
188248
db = connection_dict(config, 'database')
189-
db_name, schema_name = db_schema_split(db['database'])
249+
db_name, schema_name = get_database_schema_names(config)
190250
if schema_name:
191251
db['database'] = db_name
192252
try:
@@ -253,7 +313,7 @@ class Database(rdbms_common.Database):
253313

254314
def sql_open_connection(self):
255315
db = connection_dict(self.config, 'database')
256-
db_name, schema_name = db_schema_split (db['database'])
316+
db_name, schema_name = get_database_schema_names(self.config)
257317
if schema_name:
258318
db['database'] = db_name
259319

test/pg_service.conf

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[roundup_test_db]
2+
host=127.0.0.1
3+
port=5432
4+
user=rounduptest
5+
password=rounduptest
6+
dbname=rounduptest
7+
8+
[roundup_test_schema]
9+
host=127.0.0.1
10+
port=5432
11+
user=rounduptest_schema
12+
password=rounduptest
13+
dbname=rounduptest_schema
14+
options=-c search_path=roundup_service_dev
15+
16+
[roundup_test_schema_bad]
17+
host=127.0.0.1
18+
port=5432
19+
user=rounduptest_schema
20+
password=rounduptest
21+
dbname=rounduptest_schema
22+
options=-c search_path=
23+

0 commit comments

Comments
 (0)