1 | """
|
---|
2 | PostgreSQL database backend for Django using sqlrelay.
|
---|
3 |
|
---|
4 | Requires sqlrelay bindings: http://sqlrelay.sourceforge.net/
|
---|
5 | """
|
---|
6 |
|
---|
7 | from django.core.db import base, typecasts
|
---|
8 | from SQLRelay import PySQLRDB as Database
|
---|
9 |
|
---|
10 | DatabaseError = Database.DatabaseError
|
---|
11 |
|
---|
12 | class SQLRelayCursorWrapper(Database.SQLRCursor):
|
---|
13 | """
|
---|
14 | Django uses "format" style placeholders, but sqlrelay uses "named parameter" style.
|
---|
15 | This fixes it -- but note that if you want to use a literal "%s" in a query,
|
---|
16 | you'll need to use "%%s" (which I belive is true of other wrappers as well).
|
---|
17 | """
|
---|
18 |
|
---|
19 | def execute(self, query, params=[]):
|
---|
20 | (query, params) = self.convert_query(query, params)
|
---|
21 | return Database.SQLRCursor.execute(self, query, params)
|
---|
22 |
|
---|
23 | def executemany(self, query, params=[]):
|
---|
24 | query = self.convert_query(query, params[0])
|
---|
25 | return Database.SQLRCursor.executemany(self, query, params)
|
---|
26 |
|
---|
27 | def convert_query(self, query, params):
|
---|
28 | return (query.replace('%s', '?'), dict([(str(idx+1), v) for (idx, v) in enumerate(params)]))
|
---|
29 | qdict = {}
|
---|
30 | for idx, param in enumerate(params):
|
---|
31 | p = query.find('%s')
|
---|
32 | if p >= 0:
|
---|
33 | pname = ':val%d' % idx
|
---|
34 | query = query[:p] + pname + query[p+2:]
|
---|
35 | qdict[pname] = param
|
---|
36 | else:
|
---|
37 | raise SyntaxError, query
|
---|
38 |
|
---|
39 | class DatabaseWrapper:
|
---|
40 | def __init__(self):
|
---|
41 | self.connection = None
|
---|
42 | self.queries = []
|
---|
43 |
|
---|
44 | def cursor(self):
|
---|
45 | from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE
|
---|
46 | if self.connection is None:
|
---|
47 | if DATABASE_NAME == '':
|
---|
48 | from django.core.exceptions import ImproperlyConfigured
|
---|
49 | raise ImproperlyConfigured, "You need to specify DATABASE_NAME in your Django settings file."
|
---|
50 | self.connection = Database.connect(DATABASE_HOST, int(DATABASE_PORT or '9000'), '', DATABASE_USER, DATABASE_PASSWORD)
|
---|
51 | cursor = SQLRelayCursorWrapper(self.connection.connection)
|
---|
52 | cursor.execute("SET TIME ZONE '%s'" % TIME_ZONE)
|
---|
53 | cursor.execute("BEGIN")
|
---|
54 | if DEBUG:
|
---|
55 | return base.CursorDebugWrapper(cursor, self)
|
---|
56 | return cursor
|
---|
57 |
|
---|
58 | def commit(self):
|
---|
59 | return self.connection.commit()
|
---|
60 |
|
---|
61 | def rollback(self):
|
---|
62 | if self.connection:
|
---|
63 | return self.connection.rollback()
|
---|
64 |
|
---|
65 | def close(self):
|
---|
66 | if self.connection is not None:
|
---|
67 | self.connection.close()
|
---|
68 | self.connection = None
|
---|
69 |
|
---|
70 | def dictfetchone(cursor):
|
---|
71 | "Returns a row from the cursor as a dict"
|
---|
72 | return cursor.dictfetchone()
|
---|
73 |
|
---|
74 | def dictfetchmany(cursor, number):
|
---|
75 | "Returns a certain number of rows from a cursor as a dict"
|
---|
76 | return cursor.dictfetchmany(number)
|
---|
77 |
|
---|
78 | def dictfetchall(cursor):
|
---|
79 | "Returns all rows from a cursor as a dict"
|
---|
80 | return cursor.dictfetchall()
|
---|
81 |
|
---|
82 | def get_last_insert_id(cursor, table_name, pk_name):
|
---|
83 | cursor.execute("SELECT CURRVAL('%s_%s_seq')" % (table_name, pk_name))
|
---|
84 | return cursor.fetchone()[0]
|
---|
85 |
|
---|
86 | def get_date_extract_sql(lookup_type, table_name):
|
---|
87 | # lookup_type is 'year', 'month', 'day'
|
---|
88 | # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
|
---|
89 | return "EXTRACT('%s' FROM %s)" % (lookup_type, table_name)
|
---|
90 |
|
---|
91 | def get_date_trunc_sql(lookup_type, field_name):
|
---|
92 | # lookup_type is 'year', 'month', 'day'
|
---|
93 | # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
|
---|
94 | return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
|
---|
95 |
|
---|
96 | def get_limit_offset_sql(limit, offset=None):
|
---|
97 | sql = "LIMIT %s" % limit
|
---|
98 | if offset and offset != 0:
|
---|
99 | sql += " OFFSET %s" % offset
|
---|
100 | return sql
|
---|
101 |
|
---|
102 | def get_random_function_sql():
|
---|
103 | return "RANDOM()"
|
---|
104 |
|
---|
105 | def get_table_list(cursor):
|
---|
106 | "Returns a list of table names in the current database."
|
---|
107 | cursor.execute("""
|
---|
108 | SELECT c.relname
|
---|
109 | FROM pg_catalog.pg_class c
|
---|
110 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
---|
111 | WHERE c.relkind IN ('r', 'v', '')
|
---|
112 | AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
|
---|
113 | AND pg_catalog.pg_table_is_visible(c.oid)""")
|
---|
114 | return [row[0] for row in cursor.fetchall()]
|
---|
115 |
|
---|
116 | def get_relations(cursor, table_name):
|
---|
117 | """
|
---|
118 | Returns a dictionary of {field_index: (field_index_other_table, other_table)}
|
---|
119 | representing all relationships to the given table. Indexes are 0-based.
|
---|
120 | """
|
---|
121 | cursor.execute("""
|
---|
122 | SELECT con.conkey, con.confkey, c2.relname
|
---|
123 | FROM pg_constraint con, pg_class c1, pg_class c2
|
---|
124 | WHERE c1.oid = con.conrelid
|
---|
125 | AND c2.oid = con.confrelid
|
---|
126 | AND c1.relname = %s
|
---|
127 | AND con.contype = 'f'""", [table_name])
|
---|
128 | relations = {}
|
---|
129 | for row in cursor.fetchall():
|
---|
130 | try:
|
---|
131 | # row[0] and row[1] are like "{2}", so strip the curly braces.
|
---|
132 | relations[int(row[0][1:-1]) - 1] = (int(row[1][1:-1]) - 1, row[2])
|
---|
133 | except ValueError:
|
---|
134 | continue
|
---|
135 | return relations
|
---|
136 |
|
---|
137 | def quote_name(name):
|
---|
138 | if name.startswith('"') and name.endswith('"'):
|
---|
139 | return name # Quoting once is enough.
|
---|
140 | return '"%s"' % name
|
---|
141 |
|
---|
142 | OPERATOR_MAPPING = {
|
---|
143 | 'exact': '=',
|
---|
144 | 'iexact': 'ILIKE',
|
---|
145 | 'contains': 'LIKE',
|
---|
146 | 'icontains': 'ILIKE',
|
---|
147 | 'ne': '!=',
|
---|
148 | 'gt': '>',
|
---|
149 | 'gte': '>=',
|
---|
150 | 'lt': '<',
|
---|
151 | 'lte': '<=',
|
---|
152 | 'startswith': 'LIKE',
|
---|
153 | 'endswith': 'LIKE',
|
---|
154 | 'istartswith': 'ILIKE',
|
---|
155 | 'iendswith': 'ILIKE',
|
---|
156 | }
|
---|
157 |
|
---|
158 | # This dictionary maps Field objects to their associated PostgreSQL column
|
---|
159 | # types, as strings. Column-type strings can contain format strings; they'll
|
---|
160 | # be interpolated against the values of Field.__dict__ before being output.
|
---|
161 | # If a column type is set to None, it won't be included in the output.
|
---|
162 | DATA_TYPES = {
|
---|
163 | 'AutoField': 'serial',
|
---|
164 | 'BooleanField': 'boolean',
|
---|
165 | 'CharField': 'varchar(%(maxlength)s)',
|
---|
166 | 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
|
---|
167 | 'DateField': 'date',
|
---|
168 | 'DateTimeField': 'timestamp with time zone',
|
---|
169 | 'EmailField': 'varchar(75)',
|
---|
170 | 'FileField': 'varchar(100)',
|
---|
171 | 'FilePathField': 'varchar(100)',
|
---|
172 | 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
|
---|
173 | 'ImageField': 'varchar(100)',
|
---|
174 | 'IntegerField': 'integer',
|
---|
175 | 'IPAddressField': 'inet',
|
---|
176 | 'ManyToManyField': None,
|
---|
177 | 'NullBooleanField': 'boolean',
|
---|
178 | 'OneToOneField': 'integer',
|
---|
179 | 'PhoneNumberField': 'varchar(20)',
|
---|
180 | 'PositiveIntegerField': 'integer CHECK (%(name)s >= 0)',
|
---|
181 | 'PositiveSmallIntegerField': 'smallint CHECK (%(name)s >= 0)',
|
---|
182 | 'SlugField': 'varchar(50)',
|
---|
183 | 'SmallIntegerField': 'smallint',
|
---|
184 | 'TextField': 'text',
|
---|
185 | 'TimeField': 'time',
|
---|
186 | 'URLField': 'varchar(200)',
|
---|
187 | 'USStateField': 'varchar(2)',
|
---|
188 | }
|
---|
189 |
|
---|
190 | # Maps type codes to Django Field types.
|
---|
191 | DATA_TYPES_REVERSE = {
|
---|
192 | 16: 'BooleanField',
|
---|
193 | 21: 'SmallIntegerField',
|
---|
194 | 23: 'IntegerField',
|
---|
195 | 25: 'TextField',
|
---|
196 | 869: 'IPAddressField',
|
---|
197 | 1043: 'CharField',
|
---|
198 | 1082: 'DateField',
|
---|
199 | 1083: 'TimeField',
|
---|
200 | 1114: 'DateTimeField',
|
---|
201 | 1184: 'DateTimeField',
|
---|
202 | 1266: 'TimeField',
|
---|
203 | 1700: 'FloatField',
|
---|
204 | }
|
---|