Skip to content

Commit b9093e2

Browse files
author
Richard Jones
committed
Backend improvements.
- using Zope3's test runner now, allowing GC checks, nicer controls and coverage analysis - all RDMBS backends now have indexes on several columns - added testing of schema mutation, fixed rdbms backends handling of a couple of cases - !BETA! added postgresql backend, needs work !BETA!
1 parent f9ba214 commit b9093e2

27 files changed

+2880
-207
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ are given with the most recent entry first.
55
Feature:
66
- support confirming registration by replying to the email (sf bug 763668)
77
- support setgid and running on port < 1024 (sf patch 777528)
8+
- using Zope3's test runner now, allowing GC checks, nicer controls and
9+
coverage analysis
10+
- !BETA! added postgresql backend, needs work !BETA!
11+
- all RDMBS backends now have indexes on several columns
812

913
Fixed:
1014
- mysql documentation fixed to note requirement of 4.0+ and InnoDB
15+
- added testing of schema mutation, fixed rdbms backends handling of a
16+
couple of cases
1117

1218

1319
2003-10-?? 0.6.3

doc/postgresql.txt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
==========================
2+
PostgreSQL/psycopg Backend
3+
==========================
4+
5+
This is notes about PostreSQL backend based on the psycopg adapter for
6+
roundup issue tracker.
7+
8+
9+
Prerequisites
10+
=============
11+
12+
To use PostgreSQL as backend for storing roundup data, you should
13+
additionally install:
14+
15+
1. PostgreSQL 7.x - http://www.postgresql.org/
16+
17+
2. The psycopg python interface to PostgreSQL -
18+
http://initd.org/software/initd/psycopg
19+
20+
21+
Additional configuration
22+
========================
23+
24+
To initialise and use PostgreSQL database roundup's configuration file
25+
(config.py in the tracker's home directory) should be appended with the
26+
following constants (substituting real values, obviously):
27+
28+
POSTGRESQL_DBHOST = 'localhost'
29+
POSTGRESQL_DBUSER = 'roundup'
30+
POSTGRESQL_DBPASSWORD = 'roundup'
31+
POSTGRESQL_DBNAME = 'roundup'
32+
POSTGRESQL_PORT = 5432
33+
POSTGRESQL_DATABASE = {'host':MYSQL_DBHOST, 'port':POSTGRESQL_PORT,
34+
'user':MYSQL_DBUSER, 'password':MYSQL_DBPASSWORD,
35+
'database':MYSQL_DBNAME}
36+
37+
Also note that you can leave some values out of POSTGRESQL_DATABASE: 'host' and
38+
'port' are not necessary when connecting to a local database and 'password'
39+
is optional if postgres trusts local connections. The user specified in
40+
POSTGRESQL_DBUSER must have rights to create a new database and to connect to
41+
the "template1" database, used while initializing roundup.
42+
43+
44+
Have fun with psycopg,
45+
Federico Di Gregorio <[email protected]>
46+
47+
48+
vim: et tw=80

roundup/backends/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: __init__.py,v 1.24 2003-09-14 18:55:37 jlgijsbers Exp $
18+
# $Id: __init__.py,v 1.25 2003-10-25 22:53:26 richard Exp $
1919

2020
''' Container for the hyperdb storage backend implementations.
2121
@@ -26,16 +26,16 @@
2626
__all__ = []
2727

2828
for backend in ['anydbm', ('mysql', 'MySQLdb'), 'bsddb', 'bsddb3', 'sqlite',
29-
'metakit']:
29+
'metakit', ('postgresql', 'psycopg')]:
3030
if len(backend) == 2:
3131
backend, backend_module = backend
3232
else:
3333
backend_module = backend
3434
try:
35-
globals()[backend] = __import__('back_%s' % backend, globals())
35+
globals()[backend] = __import__('back_%s'%backend, globals())
3636
__all__.append(backend)
3737
except ImportError, e:
38-
if not str(e).startswith('No module named %s' % backend_module):
38+
if not str(e).startswith('No module named %s'%backend_module):
3939
raise
4040

4141
# vim: set filetype=python ts=4 sw=4 et si

roundup/backends/back_mysql.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,37 @@
1414
import os, shutil
1515
from MySQLdb.constants import ER
1616

17-
class Maintenance:
18-
""" Database maintenance functions """
19-
def db_nuke(self, config):
20-
"""Clear all database contents and drop database itself"""
21-
db = Database(config, 'admin')
17+
# Database maintenance functions
18+
def db_nuke(config):
19+
"""Clear all database contents and drop database itself"""
20+
db = Database(config, 'admin')
21+
try:
2222
db.sql_commit()
2323
db.sql("DROP DATABASE %s" % config.MYSQL_DBNAME)
2424
db.sql("CREATE DATABASE %s" % config.MYSQL_DBNAME)
25-
if os.path.exists(config.DATABASE):
26-
shutil.rmtree(config.DATABASE)
27-
28-
def db_exists(self, config):
29-
"""Check if database already exists"""
30-
# Yes, this is a hack, but we must must open connection without
31-
# selecting a database to prevent creation of some tables
32-
config.MYSQL_DATABASE = (config.MYSQL_DBHOST, config.MYSQL_DBUSER,
33-
config.MYSQL_DBPASSWORD)
34-
db = Database(config, 'admin')
25+
finally:
26+
db.close()
27+
if os.path.exists(config.DATABASE):
28+
shutil.rmtree(config.DATABASE)
29+
30+
def db_exists(config):
31+
"""Check if database already exists"""
32+
# Yes, this is a hack, but we must must open connection without
33+
# selecting a database to prevent creation of some tables
34+
config.MYSQL_DATABASE = (config.MYSQL_DBHOST, config.MYSQL_DBUSER,
35+
config.MYSQL_DBPASSWORD)
36+
db = Database(config, 'admin')
37+
try:
3538
db.conn.select_db(config.MYSQL_DBNAME)
3639
config.MYSQL_DATABASE = (config.MYSQL_DBHOST, config.MYSQL_DBUSER,
3740
config.MYSQL_DBPASSWORD, config.MYSQL_DBNAME)
3841
db.sql("SHOW TABLES")
3942
tables = db.sql_fetchall()
40-
if tables or os.path.exists(config.DATABASE):
41-
return 1
42-
return 0
43+
finally:
44+
db.close()
45+
if tables or os.path.exists(config.DATABASE):
46+
return 1
47+
return 0
4348

4449
class Database(Database):
4550
arg = '%s'
@@ -152,10 +157,6 @@ def create_multilink_table(self, spec, ml):
152157
print >>hyperdb.DEBUG, 'create_class', (self, sql)
153158
self.cursor.execute(sql)
154159

155-
# Static methods
156-
nuke = Maintenance().db_nuke
157-
exists = Maintenance().db_exists
158-
159160
class MysqlClass:
160161
# we're overriding this method for ONE missing bit of functionality.
161162
# look for "I can't believe it's not a toy RDBMS" below
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#
2+
# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <[email protected]>
3+
#
4+
# This module is free software, and you may redistribute it and/or modify
5+
# under the same terms as Python, so long as this copyright message and
6+
# disclaimer are retained in their original form.
7+
#
8+
# psycopg backend for roundup
9+
#
10+
11+
from roundup.backends.rdbms_common import *
12+
from roundup.backends import rdbms_common
13+
import psycopg
14+
import os, shutil
15+
16+
class Maintenance:
17+
""" Database maintenance functions """
18+
def db_nuke(self, config):
19+
"""Clear all database contents and drop database itself"""
20+
config.POSTGRESQL_DATABASE['database'] = 'template1'
21+
db = Database(config, 'admin')
22+
db.conn.set_isolation_level(0)
23+
db.sql("DROP DATABASE %s" % config.POSTGRESQL_DBNAME)
24+
db.sql("CREATE DATABASE %s" % config.POSTGRESQL_DBNAME)
25+
if os.path.exists(config.DATABASE):
26+
shutil.rmtree(config.DATABASE)
27+
config.POSTGRESQL_DATABASE['database'] = config.POSTGRESQL_DBNAME
28+
29+
def db_exists(self, config):
30+
"""Check if database already exists"""
31+
try:
32+
db = Database(config, 'admin')
33+
return 1
34+
except:
35+
return 0
36+
37+
class Database(Database):
38+
arg = '%s'
39+
40+
def open_connection(self):
41+
db = getattr(self.config, 'POSTGRESQL_DATABASE')
42+
try:
43+
self.conn = psycopg.connect(**db)
44+
except psycopg.OperationalError, message:
45+
raise DatabaseError, message
46+
47+
self.cursor = self.conn.cursor()
48+
49+
try:
50+
self.database_schema = self.load_dbschema()
51+
except:
52+
self.rollback()
53+
self.database_schema = {}
54+
self.sql("CREATE TABLE schema (schema TEXT)")
55+
self.sql("CREATE TABLE ids (name VARCHAR(255), num INT4)")
56+
57+
def close(self):
58+
self.conn.close()
59+
60+
def __repr__(self):
61+
return '<psycopgroundsql 0x%x>' % id(self)
62+
63+
def sql_fetchone(self):
64+
return self.cursor.fetchone()
65+
66+
def sql_fetchall(self):
67+
return self.cursor.fetchall()
68+
69+
def sql_stringquote(self, value):
70+
return psycopg.QuotedString(str(value))
71+
72+
def save_dbschema(self, schema):
73+
s = repr(self.database_schema)
74+
self.sql('INSERT INTO schema VALUES (%s)', (s,))
75+
76+
def load_dbschema(self):
77+
self.cursor.execute('SELECT schema FROM schema')
78+
schema = self.cursor.fetchone()
79+
if schema:
80+
return eval(schema[0])
81+
82+
def save_journal(self, classname, cols, nodeid, journaldate,
83+
journaltag, action, params):
84+
params = repr(params)
85+
entry = (nodeid, journaldate, journaltag, action, params)
86+
87+
a = self.arg
88+
sql = 'INSERT INTO %s__journal (%s) values (%s, %s, %s, %s, %s)'%(
89+
classname, cols, a, a, a, a, a)
90+
91+
if __debug__:
92+
print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
93+
94+
self.cursor.execute(sql, entry)
95+
96+
def load_journal(self, classname, cols, nodeid):
97+
sql = 'SELECT %s FROM %s__journal WHERE nodeid = %s' % (
98+
cols, classname, self.arg)
99+
100+
if __debug__:
101+
print >>hyperdb.DEBUG, 'getjournal', (self, sql, nodeid)
102+
103+
self.cursor.execute(sql, (nodeid,))
104+
res = []
105+
for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
106+
params = eval(params)
107+
res.append((nodeid, date.Date(date_stamp), user, action, params))
108+
return res
109+
110+
def create_class_table(self, spec):
111+
cols, mls = self.determine_columns(spec.properties.items())
112+
cols.append('id')
113+
cols.append('__retired__')
114+
scols = ',' . join(['"%s" VARCHAR(255)' % x for x in cols])
115+
sql = 'CREATE TABLE "_%s" (%s)' % (spec.classname, scols)
116+
117+
if __debug__:
118+
print >>hyperdb.DEBUG, 'create_class', (self, sql)
119+
120+
self.cursor.execute(sql)
121+
return cols, mls
122+
123+
def create_journal_table(self, spec):
124+
cols = ',' . join(['"%s" VARCHAR(255)' % x
125+
for x in 'nodeid date tag action params' . split()])
126+
sql = 'CREATE TABLE "%s__journal" (%s)'%(spec.classname, cols)
127+
128+
if __debug__:
129+
print >>hyperdb.DEBUG, 'create_class', (self, sql)
130+
131+
self.cursor.execute(sql)
132+
133+
def create_multilink_table(self, spec, ml):
134+
sql = '''CREATE TABLE "%s_%s" (linkid VARCHAR(255),
135+
nodeid VARCHAR(255))''' % (spec.classname, ml)
136+
137+
if __debug__:
138+
print >>hyperdb.DEBUG, 'create_class', (self, sql)
139+
140+
self.cursor.execute(sql)
141+
142+
# Static methods
143+
nuke = Maintenance().db_nuke
144+
exists = Maintenance().db_exists
145+
146+
class PsycopgClass:
147+
def find(self, **propspec):
148+
"""Get the ids of nodes in this class which link to the given nodes."""
149+
150+
if __debug__:
151+
print >>hyperdb.DEBUG, 'find', (self, propspec)
152+
153+
# shortcut
154+
if not propspec:
155+
return []
156+
157+
# validate the args
158+
props = self.getprops()
159+
propspec = propspec.items()
160+
for propname, nodeids in propspec:
161+
# check the prop is OK
162+
prop = props[propname]
163+
if not isinstance(prop, Link) and not isinstance(prop, Multilink):
164+
raise TypeError, "'%s' not a Link/Multilink property"%propname
165+
166+
# first, links
167+
l = []
168+
where = []
169+
allvalues = ()
170+
a = self.db.arg
171+
for prop, values in propspec:
172+
if not isinstance(props[prop], hyperdb.Link):
173+
continue
174+
if type(values) is type(''):
175+
allvalues += (values,)
176+
where.append('_%s = %s' % (prop, a))
177+
else:
178+
allvalues += tuple(values.keys())
179+
where.append('_%s in (%s)' % (prop, ','.join([a]*len(values))))
180+
tables = []
181+
if where:
182+
self.db.sql('SELECT id AS nodeid FROM _%s WHERE %s' % (
183+
self.classname, ' and '.join(where)), allvalues)
184+
l += [x[0] for x in self.db.sql_fetchall()]
185+
186+
# now multilinks
187+
for prop, values in propspec:
188+
vals = ()
189+
if not isinstance(props[prop], hyperdb.Multilink):
190+
continue
191+
if type(values) is type(''):
192+
vals = (values,)
193+
s = a
194+
else:
195+
vals = tuple(values.keys())
196+
s = ','.join([a]*len(values))
197+
query = 'SELECT nodeid FROM %s_%s WHERE linkid IN (%s)'%(
198+
self.classname, prop, s)
199+
self.db.sql(query, vals)
200+
l += [x[0] for x in self.db.sql_fetchall()]
201+
202+
if __debug__:
203+
print >>hyperdb.DEBUG, 'find ... ', l
204+
205+
# Remove duplicated ids
206+
d = {}
207+
for k in l:
208+
d[k] = 1
209+
return d.keys()
210+
211+
return l
212+
213+
class Class(PsycopgClass, rdbms_common.Class):
214+
pass
215+
class IssueClass(PsycopgClass, rdbms_common.IssueClass):
216+
pass
217+
class FileClass(PsycopgClass, rdbms_common.FileClass):
218+
pass
219+

0 commit comments

Comments
 (0)