| 135 | | |
|---|
| 136 | | class Index: |
|---|
| 137 | | """An index on a table column (or columns) in a database.""" |
|---|
| 138 | | |
|---|
| 139 | | def __init__(self, name, qname, tablename, colname, unique=True): |
|---|
| 140 | | self.name = name |
|---|
| 141 | | self.qname = qname |
|---|
| 142 | | self.tablename = tablename |
|---|
| 143 | | self.colname = colname |
|---|
| 144 | | self.unique = unique |
|---|
| 145 | | |
|---|
| 146 | | def __repr__(self): |
|---|
| 147 | | return ("%s.%s(%r, %r, %r, %r, unique=%r)" % |
|---|
| 148 | | (self.__module__, self.__class__.__name__, |
|---|
| 149 | | self.name, self.qname, self.tablename, |
|---|
| 150 | | self.colname, self.unique)) |
|---|
| 151 | | |
|---|
| 152 | | def __copy__(self): |
|---|
| 153 | | return self.__class__(self.name, self.qname, self.tablename, |
|---|
| 154 | | self.colname, self.unique) |
|---|
| 155 | | copy = __copy__ |
|---|
| 156 | | |
|---|
| 157 | | |
|---|
| 158 | | class IndexSet(dict): |
|---|
| 159 | | |
|---|
| 160 | | def __new__(cls, table): |
|---|
| 161 | | return dict.__new__(cls) |
|---|
| 162 | | |
|---|
| 163 | | def __init__(self, table): |
|---|
| 164 | | dict.__init__(self) |
|---|
| 165 | | self.table = table |
|---|
| 166 | | |
|---|
| 167 | | def alias(self, oldname, newname): |
|---|
| 168 | | """Add a new key for the Index with the given, existing key. |
|---|
| 169 | | |
|---|
| 170 | | Consumer code should call this method when user-supplied index |
|---|
| 171 | | names do not match the names in the database. This does not |
|---|
| 172 | | remove the old key; both keys may be used to refer to the same |
|---|
| 173 | | Index object. |
|---|
| 174 | | """ |
|---|
| 175 | | if oldname == newname: |
|---|
| 176 | | return |
|---|
| 177 | | obj = self[oldname] |
|---|
| 178 | | if newname in self: |
|---|
| 179 | | dict.__delitem__(self, newname) |
|---|
| 180 | | dict.__delitem__(self, oldname) |
|---|
| 181 | | dict.__setitem__(self, newname, obj) |
|---|
| 182 | | |
|---|
| 183 | | def __setitem__(self, key, index): |
|---|
| 184 | | """Create the specified index.""" |
|---|
| 185 | | t = self.table |
|---|
| 186 | | if t.created: |
|---|
| 187 | | t.schema.db.execute_ddl('CREATE INDEX %s ON %s (%s);' % |
|---|
| 188 | | (index.qname, t.qname, |
|---|
| 189 | | t.schema.db.quote(index.colname))) |
|---|
| 190 | | dict.__setitem__(self, key, index) |
|---|
| 191 | | |
|---|
| 192 | | def __delitem__(self, key): |
|---|
| 193 | | """Drop the specified index.""" |
|---|
| 194 | | t = self.table |
|---|
| 195 | | if t.created: |
|---|
| 196 | | t.schema.db.execute_ddl('DROP INDEX %s ON %s;' % |
|---|
| 197 | | (self[key].qname, t.qname)) |
|---|
| 198 | | dict.__delitem__(self, key) |
|---|
| 199 | | |
|---|
| 200 | | |
|---|
| 201 | | class Column: |
|---|
| 202 | | """A column in a table in a database. |
|---|
| 203 | | |
|---|
| 204 | | name: the SQL name for this table (unquoted). |
|---|
| 205 | | qname: the SQL name for this table (quoted). |
|---|
| 206 | | pytype: the Python type (the actual type object, not its name). |
|---|
| 207 | | dbtype: the database type name (as used in a CREATE TABLE statement). |
|---|
| 208 | | default: default Python value for this column for new rows. |
|---|
| 209 | | hints: a dict of implementation hints, such as precision, scale, or bytes. |
|---|
| 210 | | key: True if this column is part of the table's primary key. |
|---|
| 211 | | |
|---|
| 212 | | imperfect_type: if True, signals that we are deliberately using a |
|---|
| 213 | | database type other than the default (usually in order to handle |
|---|
| 214 | | irregular values, such as huge numbers). When comparing imperfect |
|---|
| 215 | | column values with constant values in SQL, the database must be |
|---|
| 216 | | able to cast the column value to the constant's type. If that |
|---|
| 217 | | cannot be done for the given types, then the query will be marked |
|---|
| 218 | | imperfect. |
|---|
| 219 | | autoincrement: if True, uses the database's built-in sequencing. |
|---|
| 220 | | sequence_name: for databases that use separate statements to create and |
|---|
| 221 | | drop sequences, this stores the name of the sequence. |
|---|
| 222 | | initial: if autoincrement, holds the initial value for the sequence. |
|---|
| 223 | | """ |
|---|
| 224 | | |
|---|
| 225 | | def __init__(self, pytype, dbtype, default=None, hints=None, key=False, |
|---|
| 226 | | name=None, qname=None): |
|---|
| 227 | | self.pytype = pytype |
|---|
| 228 | | self.dbtype = dbtype |
|---|
| 229 | | self.name = name |
|---|
| 230 | | self.qname = qname |
|---|
| 231 | | self.default = default |
|---|
| 232 | | if hints is None: |
|---|
| 233 | | hints = {} |
|---|
| 234 | | else: |
|---|
| 235 | | hints = hints.copy() |
|---|
| 236 | | self.hints = hints |
|---|
| 237 | | self.key = key |
|---|
| 238 | | |
|---|
| 239 | | # If autoincrement, the initial value should be put in self.initial. |
|---|
| 240 | | self.autoincrement = False |
|---|
| 241 | | self.sequence_name = None |
|---|
| 242 | | self.initial = 1 |
|---|
| 243 | | |
|---|
| 244 | | self.imperfect_type = False |
|---|
| 245 | | |
|---|
| 246 | | def __repr__(self): |
|---|
| 247 | | return ("%s.%s(%r, %r, default=%r, hints=%r, key=%r, name=%r, qname=%r)" % |
|---|
| 248 | | (self.__module__, self.__class__.__name__, |
|---|
| 249 | | self.pytype, self.dbtype, |
|---|
| 250 | | self.default, self.hints, self.key, |
|---|
| 251 | | self.name, self.qname) |
|---|
| 252 | | ) |
|---|
| 253 | | |
|---|
| 254 | | def __copy__(self): |
|---|
| 255 | | newcol = self.__class__(self.pytype, self.dbtype, |
|---|
| 256 | | self.default, self.hints, self.key, |
|---|
| 257 | | self.name, self.qname) |
|---|
| 258 | | newcol.autoincrement = self.autoincrement |
|---|
| 259 | | newcol.initial = self.initial |
|---|
| 260 | | newcol.imperfect_type = self.imperfect_type |
|---|
| 261 | | return newcol |
|---|
| 262 | | copy = __copy__ |
|---|
| 263 | | |
|---|
| 264 | | |
|---|
| 265 | | class Table(dict): |
|---|
| 266 | | """A table in a database; a dict of Column objects. |
|---|
| 267 | | |
|---|
| 268 | | Values in this dict must be instances of Column (or a subclass of it). |
|---|
| 269 | | Keys should be consumer-friendly names for each Column value. |
|---|
| 270 | | |
|---|
| 271 | | name: the SQL name for this table (unquoted). |
|---|
| 272 | | qname: the SQL name for this table (quoted). |
|---|
| 273 | | schema: the schema for this table. |
|---|
| 274 | | created: whether or not this Table has a concrete implementation in the |
|---|
| 275 | | database. If False (the default), then changes to Table items can be |
|---|
| 276 | | made with impunity. If True, then appropriate ALTER TABLE commands |
|---|
| 277 | | are executed whenever a consumer adds or deletes items from the |
|---|
| 278 | | Table, or calls methods like 'rename'. |
|---|
| 279 | | indices: a dict-like IndexSet of Index objects. |
|---|
| 280 | | references: a dict of the form: {name: (nearColKey, farTableKey, farColKey)}. |
|---|
| 281 | | """ |
|---|
| 282 | | |
|---|
| 283 | | def __new__(cls, name, qname, schema, created=False): |
|---|
| 284 | | return dict.__new__(cls) |
|---|
| 285 | | |
|---|
| 286 | | def __init__(self, name, qname, schema, created=False): |
|---|
| 287 | | dict.__init__(self) |
|---|
| 288 | | |
|---|
| 289 | | self.name = name |
|---|
| 290 | | self.qname = qname |
|---|
| 291 | | self.schema = schema |
|---|
| 292 | | self.created = created |
|---|
| 293 | | |
|---|
| 294 | | self.indices = schema.indexsetclass(self) |
|---|
| 295 | | self.references = {} |
|---|
| 296 | | |
|---|
| 297 | | def __repr__(self): |
|---|
| 298 | | name = getattr(self, "name", "<unknown>") |
|---|
| 299 | | qname = getattr(self, "qname", "<unknown>") |
|---|
| 300 | | return ("%s.%s(%r, %r)" % |
|---|
| 301 | | (self.__module__, self.__class__.__name__, name, qname)) |
|---|
| 302 | | |
|---|
| 303 | | def __copy__(self): |
|---|
| 304 | | # Don't set 'created' when copying! |
|---|
| 305 | | newtable = self.__class__(self.name, self.qname, self.schema) |
|---|
| 306 | | for key, c in self.iteritems(): |
|---|
| 307 | | dict.__setitem__(newtable, key, c.copy()) |
|---|
| 308 | | for key, i in self.indices.iteritems(): |
|---|
| 309 | | dict.__setitem__(newtable.indices, key, i.copy()) |
|---|
| 310 | | return newtable |
|---|
| 311 | | copy = __copy__ |
|---|
| 312 | | |
|---|
| 313 | | def alias(self, oldname, newname): |
|---|
| 314 | | """Add a new key for the Column with the given, existing key. |
|---|
| 315 | | |
|---|
| 316 | | Consumer code should call this method when user-supplied column |
|---|
| 317 | | names do not match the names in the database. This does not |
|---|
| 318 | | remove the old key; both keys may be used to refer to the same |
|---|
| 319 | | Column object. |
|---|
| 320 | | """ |
|---|
| 321 | | if oldname == newname: |
|---|
| 322 | | return |
|---|
| 323 | | |
|---|
| 324 | | obj = self[oldname] |
|---|
| 325 | | if newname in self: |
|---|
| 326 | | dict.__delitem__(self, newname) |
|---|
| 327 | | dict.__delitem__(self, oldname) |
|---|
| 328 | | dict.__setitem__(self, newname, obj) |
|---|
| 329 | | |
|---|
| 330 | | def _add_column(self, column): |
|---|
| 331 | | """Internal function to add the column to the database.""" |
|---|
| 332 | | coldef = self.schema.columnclause(column) |
|---|
| 333 | | self.schema.db.execute("ALTER TABLE %s ADD COLUMN %s;" % |
|---|
| 334 | | (self.qname, coldef)) |
|---|
| 335 | | |
|---|
| 336 | | def __setitem__(self, key, column): |
|---|
| 337 | | if column.name is None: |
|---|
| 338 | | column.name = self.schema._column_name(self.name, key) |
|---|
| 339 | | column.qname = self.schema.db.quote(column.name) |
|---|
| 340 | | |
|---|
| 341 | | if not self.created: |
|---|
| 342 | | dict.__setitem__(self, key, column) |
|---|
| 343 | | return |
|---|
| 344 | | |
|---|
| 345 | | if key in self: |
|---|
| 346 | | del self[key] |
|---|
| 347 | | |
|---|
| 348 | | if column.autoincrement: |
|---|
| 349 | | # This may or may not be a no-op, depending on the DB. |
|---|
| 350 | | self.schema.create_sequence(self, column) |
|---|
| 351 | | self._add_column(column) |
|---|
| 352 | | dict.__setitem__(self, key, column) |
|---|
| 353 | | |
|---|
| 354 | | def _drop_column(self, column): |
|---|
| 355 | | """Internal function to drop the column from the database.""" |
|---|
| 356 | | self.schema.db.execute_ddl("ALTER TABLE %s DROP COLUMN %s;" % |
|---|
| 357 | | (self.qname, column.qname)) |
|---|
| 358 | | |
|---|
| 359 | | def __delitem__(self, key): |
|---|
| 360 | | if key in self.indices: |
|---|
| 361 | | del self.indices[key] |
|---|
| 362 | | |
|---|
| 363 | | if not self.created: |
|---|
| 364 | | dict.__delitem__(self, key) |
|---|
| 365 | | return |
|---|
| 366 | | |
|---|
| 367 | | column = self[key] |
|---|
| 368 | | self._drop_column(column) |
|---|
| 369 | | if column.autoincrement: |
|---|
| 370 | | # This may or may not be a no-op, depending on the DB. |
|---|
| 371 | | self.schema.drop_sequence(column) |
|---|
| 372 | | dict.__delitem__(self, key) |
|---|
| 373 | | |
|---|
| 374 | | def _rename(self, oldcol, newcol): |
|---|
| 375 | | # Override this to do the actual rename at the DB level. |
|---|
| 376 | | self.schema.db.execute_ddl("ALTER TABLE %s RENAME COLUMN %s TO %s;" % |
|---|
| 377 | | (self.qname, oldcol.qname, newcol.qname)) |
|---|
| 378 | | |
|---|
| 379 | | def rename(self, oldkey, newkey): |
|---|
| 380 | | """Rename a Column. This will change the table name in the database.""" |
|---|
| 381 | | oldcol = self[oldkey] |
|---|
| 382 | | |
|---|
| 383 | | if not self.created: |
|---|
| 384 | | dict.__delitem__(self, oldkey) |
|---|
| 385 | | dict.__setitem__(self, newkey, oldcol) |
|---|
| 386 | | return |
|---|
| 387 | | |
|---|
| 388 | | oldname = oldcol.name |
|---|
| 389 | | newname = self.schema._column_name(self.name, newkey) |
|---|
| 390 | | |
|---|
| 391 | | if oldname != newname: |
|---|
| 392 | | newcol = oldcol.copy() |
|---|
| 393 | | newcol.name = newname |
|---|
| 394 | | newcol.qname = self.schema.db.quote(newname) |
|---|
| 395 | | self._rename(oldcol, newcol) |
|---|
| 396 | | |
|---|
| 397 | | # Use the superclass calls to avoid DROP COLUMN/ADD COLUMN. |
|---|
| 398 | | dict.__delitem__(self, oldkey) |
|---|
| 399 | | dict.__setitem__(self, newkey, newcol) |
|---|
| 400 | | |
|---|
| 401 | | def add_index(self, columnkey): |
|---|
| 402 | | """Add and return a new Index for the given column key. |
|---|
| 403 | | |
|---|
| 404 | | The new Index object will possess the same key as the column. |
|---|
| 405 | | In general, the actual SQL name of the new Index will be of |
|---|
| 406 | | the form: "i" + table.name + column.name. |
|---|
| 407 | | """ |
|---|
| 408 | | colname = self[columnkey].name |
|---|
| 409 | | name = self.schema.table_name("i" + self.name + colname) |
|---|
| 410 | | i = Index(name, self.schema.db.quote(name), self.name, colname) |
|---|
| 411 | | self.indices[columnkey] = i |
|---|
| 412 | | return i |
|---|
| 413 | | |
|---|
| 414 | | |
|---|
| 415 | | # ---------------------------- OLTP/CRUD ---------------------------- # |
|---|
| 416 | | |
|---|
| 417 | | def whereclause(self, **inputs): |
|---|
| 418 | | """Return an SQL WHERE clause for the given input fields. |
|---|
| 419 | | |
|---|
| 420 | | If the given clause is imperfect, a ValueError is raised. |
|---|
| 421 | | """ |
|---|
| 422 | | tpair = [(self.qname, self)] |
|---|
| 423 | | decom = self.schema.db.decompiler(tpair, logic.filter(**inputs), |
|---|
| 424 | | self.schema.db.adaptertosql) |
|---|
| 425 | | code = decom.code() |
|---|
| 426 | | if decom.imperfect: |
|---|
| 427 | | raise ValueError("The given inputs could not safely be translated " |
|---|
| 428 | | "to SQL.", inputs, code) |
|---|
| 429 | | return code |
|---|
| 430 | | |
|---|
| 431 | | def id_clause(self, **inputs): |
|---|
| 432 | | """Return an SQL expression for the identifiers of the given table.""" |
|---|
| 433 | | for key in inputs.keys(): |
|---|
| 434 | | if not self[key].key: |
|---|
| 435 | | inputs.pop(key) |
|---|
| 436 | | return self.whereclause(**inputs) |
|---|
| 437 | | |
|---|
| 438 | | def insert(self, **inputs): |
|---|
| 439 | | """Insert a row and return {idcolkey: newid}.""" |
|---|
| 440 | | coerce_out = self.schema.db.adaptertosql.coerce |
|---|
| 441 | | coerce_in = self.schema.db.adapterfromdb.coerce |
|---|
| 442 | | |
|---|
| 443 | | fields = [] |
|---|
| 444 | | idkeys = [] |
|---|
| 445 | | values = [] |
|---|
| 446 | | for key, col in self.iteritems(): |
|---|
| 447 | | if col.autoincrement: |
|---|
| 448 | | # Skip this field, since we're using a sequencer |
|---|
| 449 | | idkeys.append(key) |
|---|
| 450 | | continue |
|---|
| 451 | | if key in inputs: |
|---|
| 452 | | val = coerce_out(inputs[key], col.dbtype) |
|---|
| 453 | | fields.append(col.qname) |
|---|
| 454 | | values.append(val) |
|---|
| 455 | | |
|---|
| 456 | | conn = self.schema.db.connections.get() |
|---|
| 457 | | |
|---|
| 458 | | fields = ", ".join(fields) |
|---|
| 459 | | values = ", ".join(values) |
|---|
| 460 | | self.schema.db.execute('INSERT INTO %s (%s) VALUES (%s);' % |
|---|
| 461 | | (self.qname, fields, values), conn) |
|---|
| 462 | | |
|---|
| 463 | | if idkeys: |
|---|
| 464 | | newids = self._grab_new_ids(idkeys, conn) |
|---|
| 465 | | for key in newids.keys(): |
|---|
| 466 | | col = self[key] |
|---|
| 467 | | newids[key] = coerce_in(newids[key], col.dbtype, col.pytype) |
|---|
| 468 | | return newids |
|---|
| 469 | | else: |
|---|
| 470 | | return {} |
|---|
| 471 | | |
|---|
| 472 | | def _grab_new_ids(self, idkeys, conn): |
|---|
| 473 | | # Override this to fetch and return new autoincrement values. |
|---|
| 474 | | raise NotImplementedError |
|---|
| 475 | | |
|---|
| 476 | | def save(self, **inputs): |
|---|
| 477 | | """Update a row using the given inputs.""" |
|---|
| 478 | | parms = [] |
|---|
| 479 | | coerce = self.schema.db.adaptertosql.coerce |
|---|
| 480 | | for key, val in inputs.iteritems(): |
|---|
| 481 | | col = self[key] |
|---|
| 482 | | if col.autoincrement: |
|---|
| 483 | | # Skip this field, since we're using a sequencer |
|---|
| 484 | | pass |
|---|
| 485 | | else: |
|---|
| 486 | | val = coerce(val, col.dbtype) |
|---|
| 487 | | parms.append('%s = %s' % (col.qname, val)) |
|---|
| 488 | | |
|---|
| 489 | | if parms: |
|---|
| 490 | | sql = ('UPDATE %s SET %s WHERE %s;' % |
|---|
| 491 | | (self.qname, ", ".join(parms), self.id_clause(**inputs))) |
|---|
| 492 | | self.schema.db.execute(sql) |
|---|
| 493 | | |
|---|
| 494 | | use_asterisk_to_delete_all = False |
|---|
| 495 | | |
|---|
| 496 | | def delete(self, **inputs): |
|---|
| 497 | | """Delete all rows matching the given identifier inputs.""" |
|---|
| 498 | | if self.use_asterisk_to_delete_all: |
|---|
| 499 | | star = " *" |
|---|
| 500 | | else: |
|---|
| 501 | | star = "" |
|---|
| 502 | | self.schema.db.execute('DELETE%s FROM %s WHERE %s;' % |
|---|
| 503 | | (star, self.qname, self.id_clause(**inputs))) |
|---|
| 504 | | |
|---|
| 505 | | def delete_all(self, **inputs): |
|---|
| 506 | | """Delete all rows matching the given inputs.""" |
|---|
| 507 | | if self.use_asterisk_to_delete_all: |
|---|
| 508 | | star = " *" |
|---|
| 509 | | else: |
|---|
| 510 | | star = "" |
|---|
| 511 | | self.schema.db.execute('DELETE%s FROM %s WHERE %s;' % |
|---|
| 512 | | (star, self.qname, |
|---|
| 513 | | self.whereclause(**inputs))) |
|---|
| 514 | | |
|---|
| 515 | | def select_all(self, restriction=None, **kwargs): |
|---|
| 516 | | """Yield data dicts matching the given restriction.""" |
|---|
| 517 | | if restriction and not isinstance(restriction, logic.Expression): |
|---|
| 518 | | restriction = logic.Expression(restriction) |
|---|
| 519 | | if kwargs: |
|---|
| 520 | | f = logic.filter(**kwargs) |
|---|
| 521 | | if restriction: |
|---|
| 522 | | restriction += f |
|---|
| 523 | | else: |
|---|
| 524 | | restriction = f |
|---|
| 525 | | |
|---|
| 526 | | attrs = self.keys() |
|---|
| 527 | | data = self.schema.db.select(self, attrs, restriction) |
|---|
| 528 | | for row in data: |
|---|
| 529 | | row = dict(zip(attrs, row)) |
|---|
| 530 | | if restriction and data.imperfect: |
|---|
| 531 | | # Run a dummy object through our restriction before yielding. |
|---|
| 532 | | if not restriction(ImperfectDummy(**row)): |
|---|
| 533 | | continue |
|---|
| 534 | | yield row |
|---|
| 535 | | |
|---|
| 536 | | def select_one(self, restriction=None, **kwargs): |
|---|
| 537 | | """Return a single data dict matching the given restriction (or None).""" |
|---|
| 538 | | try: |
|---|
| 539 | | return self.select_all(restriction, **kwargs).next() |
|---|
| 540 | | except StopIteration: |
|---|
| 541 | | return None |
|---|
| 542 | | |
|---|
| 543 | | |
|---|
| 544 | | class ImperfectDummy(object): |
|---|
| 545 | | """A dummy object for resolving imperfect queries.""" |
|---|
| 546 | | def __init__(self, **kwargs): |
|---|
| 547 | | for k, v in kwargs.iteritems(): |
|---|
| 548 | | setattr(self, k, v) |
|---|
| 549 | | |
|---|
| 550 | | |
|---|
| 551 | | class Schema(dict): |
|---|
| 552 | | """A dict for managing a set of tables. |
|---|
| 553 | | |
|---|
| 554 | | Values in this dict must be instances of Table. Keys should be |
|---|
| 555 | | consumer-friendly names for each Table value. For example, it's |
|---|
| 556 | | easiest to use all lowercase table names in MySQL; however, a |
|---|
| 557 | | geniusql consumer might want their code to use TitledNames to |
|---|
| 558 | | refer to each table. |
|---|
| 559 | | |
|---|
| 560 | | When a consumer adds and deletes items from a Schema object, |
|---|
| 561 | | appropriate CREATE TABLE/DROP TABLE commands are executed. |
|---|
| 562 | | This means that a Table object to be added should have all |
|---|
| 563 | | of its columns populated before adding it to the Schema. |
|---|
| 564 | | """ |
|---|
| 565 | | |
|---|
| 566 | | tableclass = Table |
|---|
| 567 | | indexsetclass = IndexSet |
|---|
| 568 | | |
|---|
| 569 | | def __new__(cls, db, name): |
|---|
| 570 | | return dict.__new__(cls) |
|---|
| 571 | | |
|---|
| 572 | | def __init__(self, db, name): |
|---|
| 573 | | dict.__init__(self) |
|---|
| 574 | | |
|---|
| 575 | | self.db = db |
|---|
| 576 | | self.name = self.db.sql_name(name) |
|---|
| 577 | | self.qname = self.db.quote(self.name) |
|---|
| 578 | | self._discover_lock = threading.Lock() |
|---|
| 579 | | self.discover_dbinfo() |
|---|
| 580 | | |
|---|
| 581 | | def __repr__(self): |
|---|
| 582 | | name = getattr(self, "name", "<unknown>") |
|---|
| 583 | | return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, name) |
|---|
| 584 | | |
|---|
| 585 | | # Discovery # |
|---|
| 586 | | |
|---|
| 587 | | def _get_dbinfo(self, conn=None): |
|---|
| 588 | | return {} |
|---|
| 589 | | |
|---|
| 590 | | def discover_dbinfo(self, conn=None): |
|---|
| 591 | | """Set attributes on self with actual DB metadata, where possible.""" |
|---|
| 592 | | for k, v in self._get_dbinfo().iteritems(): |
|---|
| 593 | | setattr(self, k, v) |
|---|
| 594 | | |
|---|
| 595 | | def _get_tables(self, conn=None): |
|---|
| 596 | | raise NotImplementedError |
|---|
| 597 | | |
|---|
| 598 | | def _get_table(self, tablename, conn=None): |
|---|
| 599 | | # Fallback behavior. This is slow and should be optimized by each DB. |
|---|
| 600 | | for t in self._get_tables(): |
|---|
| 601 | | if t.name == tablename: |
|---|
| 602 | | return t |
|---|
| 603 | | raise errors.MappingError(tablename) |
|---|
| 604 | | |
|---|
| 605 | | def _get_columns(self, tablename, conn=None): |
|---|
| 606 | | raise NotImplementedError |
|---|
| 607 | | |
|---|
| 608 | | def _get_indices(self, tablename, conn=None): |
|---|
| 609 | | raise NotImplementedError |
|---|
| 610 | | |
|---|
| 611 | | def _discover_table(self, table, conn=None): |
|---|
| 612 | | """Populate the columns and indices of the given Table object.""" |
|---|
| 613 | | for col in self._get_columns(table.name, conn): |
|---|
| 614 | | # Use the superclass call to avoid ALTER TABLE |
|---|
| 615 | | if col.name in table: |
|---|
| 616 | | dict.__delitem__(table, col.name) |
|---|
| 617 | | dict.__setitem__(table, col.name, col) |
|---|
| 618 | | |
|---|
| 619 | | for idx in self._get_indices(table.name, conn): |
|---|
| 620 | | # Use the superclass call to avoid CREATE INDEX |
|---|
| 621 | | if idx.name in table.indices: |
|---|
| 622 | | dict.__delitem__(table.indices, idx.name) |
|---|
| 623 | | dict.__setitem__(table.indices, idx.name, idx) |
|---|
| 624 | | |
|---|
| 625 | | def discover(self, tablename, conn=None): |
|---|
| 626 | | """Attach a new Table from the underlying DB to self (and return it). |
|---|
| 627 | | |
|---|
| 628 | | Table objects (and their Column and Index subobjects) will be |
|---|
| 629 | | added to self using keys that match the database's names. |
|---|
| 630 | | Consumers should call the "alias(oldname, newname)" method |
|---|
| 631 | | of Schema, Table, and IndexSet in order to re-map the |
|---|
| 632 | | discovered objects using consumer-friendly names. |
|---|
| 633 | | |
|---|
| 634 | | If no such table exists, a MappingError should be raised. |
|---|
| 635 | | """ |
|---|
| 636 | | self._discover_lock.acquire() |
|---|
| 637 | | try: |
|---|
| 638 | | table = self._get_table(tablename) |
|---|
| 639 | | self._discover_table(table, conn) |
|---|
| 640 | | |
|---|
| 641 | | # Use the superclass calls to avoid CREATE TABLE |
|---|
| 642 | | if table.name in self: |
|---|
| 643 | | dict.__delitem__(self, table.name) |
|---|
| 644 | | dict.__setitem__(self, table.name, table) |
|---|
| 645 | | |
|---|
| 646 | | return table |
|---|
| 647 | | finally: |
|---|
| 648 | | self._discover_lock.release() |
|---|
| 649 | | |
|---|
| 650 | | def discover_all(self, conn=None): |
|---|
| 651 | | """(Re-)populate self (all table items) from the underlying DB. |
|---|
| 652 | | |
|---|
| 653 | | Table objects (and their Column and Index subobjects) will be |
|---|
| 654 | | added to self using keys that match the database's names. |
|---|
| 655 | | Consumers should call the "alias(oldname, newname)" method |
|---|
| 656 | | of Schema, Table, and IndexSet in order to re-map the |
|---|
| 657 | | discovered objects using consumer-friendly names. |
|---|
| 658 | | |
|---|
| 659 | | This method is idempotent, but that doesn't mean cheap. Try not |
|---|
| 660 | | to call it very often (once at app startup is usually enough). |
|---|
| 661 | | If you already know the names of all the tables you want to |
|---|
| 662 | | discover, it's often faster to skip this method and just use |
|---|
| 663 | | the discover(tablename) method for each known name instead. |
|---|
| 664 | | """ |
|---|
| 665 | | self._discover_lock.acquire() |
|---|
| 666 | | try: |
|---|
| 667 | | for table in self._get_tables(conn): |
|---|
| 668 | | self._discover_table(table, conn) |
|---|
| 669 | | |
|---|
| 670 | | # Use the superclass calls to avoid CREATE TABLE |
|---|
| 671 | | if table.name in self: |
|---|
| 672 | | dict.__delitem__(self, table.name) |
|---|
| 673 | | dict.__setitem__(self, table.name, table) |
|---|
| 674 | | finally: |
|---|
| 675 | | self._discover_lock.release() |
|---|
| 676 | | |
|---|
| 677 | | def alias(self, oldname, newname): |
|---|
| 678 | | """Add a new key for the Table with the given, existing key. |
|---|
| 679 | | |
|---|
| 680 | | Consumer code should call this method when user-supplied table |
|---|
| 681 | | names do not match the names in the database. This does not |
|---|
| 682 | | remove the old key; both keys may be used to refer to the same |
|---|
| 683 | | Table object. |
|---|
| 684 | | """ |
|---|
| 685 | | if oldname == newname: |
|---|
| 686 | | return |
|---|
| 687 | | |
|---|
| 688 | | obj = self[oldname] |
|---|
| 689 | | if newname in self: |
|---|
| 690 | | dict.__delitem__(self, newname) |
|---|
| 691 | | dict.__delitem__(self, oldname) |
|---|
| 692 | | dict.__setitem__(self, newname, obj) |
|---|
| 693 | | |
|---|
| 694 | | def _column_name(self, tablename, columnkey): |
|---|
| 695 | | "Return the SQL column name for the given table name and column key." |
|---|
| 696 | | # If you want to use a map from your ORM's property names |
|---|
| 697 | | # to DB column names, override this method (that's why |
|---|
| 698 | | # the tablename must be included in the args). |
|---|
| 699 | | return self.db.sql_name(columnkey) |
|---|
| 700 | | |
|---|
| 701 | | def column(self, pytype=unicode, dbtype=None, default=None, hints=None, |
|---|
| 702 | | key=False, autoincrement=False): |
|---|
| 703 | | """Return a Column object from the given arguments.""" |
|---|
| 704 | | col = Column(pytype, dbtype, default, hints, key) |
|---|
| 705 | | col.autoincrement = autoincrement |
|---|
| 706 | | |
|---|
| 707 | | if dbtype is None: |
|---|
| 708 | | col.dbtype = self.db.typeadapter.coerce(col, pytype) |
|---|
| 709 | | pytype2 = self.db.python_type(col.dbtype) |
|---|
| 710 | | col.imperfect_type = not self.db.isrelatedtype(pytype, pytype2) |
|---|
| 711 | | |
|---|
| 712 | | return col |
|---|
| 713 | | |
|---|
| 714 | | prefix = "" |
|---|
| 715 | | |
|---|
| 716 | | def table_name(self, key): |
|---|
| 717 | | """Return the SQL table name for the given key.""" |
|---|
| 718 | | # If you want to use a map from your ORM's class names |
|---|
| 719 | | # to DB table names, override this method. |
|---|
| 720 | | return self.db.sql_name(self.prefix + key) |
|---|
| 721 | | |
|---|
| 722 | | def table(self, name): |
|---|
| 723 | | """Create and return a Table object for the given name.""" |
|---|
| 724 | | name = self.table_name(name) |
|---|
| 725 | | return self.tableclass(name, self.db.quote(name), self) |
|---|
| 726 | | |
|---|
| 727 | | def create_sequence(self, table, column): |
|---|
| 728 | | """Create a SEQUENCE for the given column and set its sequence_name.""" |
|---|
| 729 | | # By default, this does nothing. Databases which require a separate |
|---|
| 730 | | # statement to create a sequence generator should override this. |
|---|
| 731 | | pass |
|---|
| 732 | | |
|---|
| 733 | | def drop_sequence(self, column): |
|---|
| 734 | | """Drop a SEQUENCE for the given column and remove its sequence_name.""" |
|---|
| 735 | | # By default, this does nothing. Databases which require a separate |
|---|
| 736 | | # statement to drop a sequence generator should override this. |
|---|
| 737 | | pass |
|---|
| 738 | | |
|---|
| 739 | | def columnclause(self, column): |
|---|
| 740 | | """Return a clause for the given column for CREATE or ALTER TABLE. |
|---|
| 741 | | |
|---|
| 742 | | This will be of the form "name type [DEFAULT x]". |
|---|
| 743 | | |
|---|
| 744 | | Most subclasses will override this to add autoincrement support. |
|---|
| 745 | | """ |
|---|
| 746 | | dbtype = column.dbtype |
|---|
| 747 | | |
|---|
| 748 | | default = column.default or "" |
|---|
| 749 | | if default: |
|---|
| 750 | | default = self.db.adaptertosql.coerce(default, dbtype) |
|---|
| 751 | | default = " DEFAULT %s" % default |
|---|
| 752 | | |
|---|
| 753 | | return "%s %s%s" % (column.qname, dbtype, default) |
|---|
| 754 | | |
|---|
| 755 | | def __setitem__(self, key, table): |
|---|
| 756 | | if key in self: |
|---|
| 757 | | del self[key] |
|---|
| 758 | | |
|---|
| 759 | | # Set table.created to True, which should "turn on" |
|---|
| 760 | | # any future ALTER TABLE statements. |
|---|
| 761 | | table.created = True |
|---|
| 762 | | |
|---|
| 763 | | fields = [] |
|---|
| 764 | | pk = [] |
|---|
| 765 | | for column in table.itervalues(): |
|---|
| 766 | | if column.autoincrement: |
|---|
| 767 | | # This may or may not be a no-op, depending on the DB. |
|---|
| 768 | | self.create_sequence(table, column) |
|---|
| 769 | | |
|---|
| 770 | | fields.append(self.columnclause(column)) |
|---|
| 771 | | if column.key: |
|---|
| 772 | | pk.append(column.qname) |
|---|
| 773 | | |
|---|
| 774 | | if pk: |
|---|
| 775 | | pk = ", PRIMARY KEY (%s)" % ", ".join(pk) |
|---|
| 776 | | else: |
|---|
| 777 | | pk = "" |
|---|
| 778 | | |
|---|
| 779 | | self.db.execute_ddl('CREATE TABLE %s (%s%s);' % |
|---|
| 780 | | (table.qname, ", ".join(fields), pk)) |
|---|
| 781 | | |
|---|
| 782 | | for index in table.indices.itervalues(): |
|---|
| 783 | | self.db.execute_ddl('CREATE INDEX %s ON %s (%s);' % |
|---|
| 784 | | (index.qname, table.qname, |
|---|
| 785 | | self.db.quote(index.colname))) |
|---|
| 786 | | |
|---|
| 787 | | dict.__setitem__(self, key, table) |
|---|
| 788 | | |
|---|
| 789 | | def __delitem__(self, key): |
|---|
| 790 | | table = self[key] |
|---|
| 791 | | self.db.execute_ddl('DROP TABLE %s;' % table.qname) |
|---|
| 792 | | for col in table.itervalues(): |
|---|
| 793 | | if col.autoincrement: |
|---|
| 794 | | self.drop_sequence(col) |
|---|
| 795 | | dict.__delitem__(self, key) |
|---|
| 796 | | |
|---|
| 797 | | def _rename(self, oldtable, newtable): |
|---|
| 798 | | # Override this to do the actual rename at the DB level. |
|---|
| 799 | | raise NotImplementedError |
|---|
| 800 | | newtable.created = True |
|---|
| 801 | | |
|---|
| 802 | | def rename(self, oldkey, newkey): |
|---|
| 803 | | """Rename a Table.""" |
|---|
| 804 | | oldtable = self[oldkey] |
|---|
| 805 | | oldname = oldtable.name |
|---|
| 806 | | newname = self.db.table_name(newkey) |
|---|
| 807 | | |
|---|
| 808 | | if oldname != newname: |
|---|
| 809 | | newtable = oldtable.copy() |
|---|
| 810 | | newtable.schema = self.schema |
|---|
| 811 | | newtable.name = newname |
|---|
| 812 | | newtable.qname = self.db.quote(newname) |
|---|
| 813 | | self._rename(oldtable, newname) |
|---|
| 814 | | |
|---|
| 815 | | # Use the superclass calls to avoid DROP TABLE/CREATE TABLE. |
|---|
| 816 | | dict.__delitem__(self, oldkey) |
|---|
| 817 | | dict.__setitem__(self, newkey, newtable) |
|---|
| 818 | | |
|---|
| 819 | | def create_database(self): |
|---|
| 820 | | self.db.execute_ddl("CREATE DATABASE %s;" % self.qname) |
|---|
| 821 | | self.clear() |
|---|
| 822 | | |
|---|
| 823 | | def drop_database(self): |
|---|
| 824 | | # Must shut down all connections to avoid |
|---|
| 825 | | # "being accessed by other users" error. |
|---|
| 826 | | self.db.connections.shutdown() |
|---|
| 827 | | self.db.execute_ddl("DROP DATABASE %s;" % self.qname) |
|---|
| 828 | | self.clear() |
|---|
| 829 | | |
|---|
| 830 | | |
|---|
| 831 | | class Database(object): |
|---|
| 832 | | |
|---|
| 833 | | __metaclass__ = _AttributeDocstrings |
|---|
| 834 | | |
|---|
| 835 | | adaptertosql = AdapterToSQL() |
|---|
| 836 | | adapterfromdb = AdapterFromDB() |
|---|
| 837 | | typeadapter = TypeAdapter() |
|---|
| 838 | | decompiler = SQLDecompiler |
|---|
| 839 | | joinwrapper = TableWrapper |
|---|
| 840 | | selectwriter = SelectWriter |
|---|
| 841 | | connectionmanager = ConnectionManager |
|---|
| 842 | | schemaclass = Schema |
|---|
| 843 | | |
|---|
| 844 | | multischema = True |
|---|
| 845 | | multischema__doc = """If True, instances of this Database class may |
|---|
| 846 | | spawn multiple Schema instances. This is False, for example, when |
|---|
| 847 | | the underlying Database engine binds connections to individual files. |
|---|
| 848 | | In most applications (that use a single schema) this presents no |
|---|
| 849 | | problems; applications that need to handle more than one schema |
|---|
| 850 | | at a time should inspect this value to determine whether they |
|---|
| 851 | | need a separate Database instance per Schema instance. |
|---|
| 852 | | """ |
|---|
| 853 | | |
|---|
| 854 | | def __init__(self, **kwargs): |
|---|
| 855 | | for k, v in kwargs.iteritems(): |
|---|
| 856 | | setattr(self, k, v) |
|---|
| 857 | | |
|---|
| 858 | | poolsize = kwargs.get('poolsize', 10) |
|---|
| 859 | | self.connections = self.connectionmanager(self, poolsize) |
|---|
| 860 | | |
|---|
| 861 | | def version(self): |
|---|
| 862 | | """Return a string containing version info for this database.""" |
|---|
| 863 | | raise NotImplementedError |
|---|
| 864 | | |
|---|
| 865 | | def log(self, msg): |
|---|
| 866 | | pass |
|---|
| 867 | | |
|---|
| 868 | | def python_type(self, dbtype): |
|---|
| 869 | | """Return a Python type which can store values of the given dbtype.""" |
|---|
| 870 | | raise TypeError("Database type %r could not be converted " |
|---|
| 871 | | "to a Python type." % dbtype) |
|---|
| 872 | | |
|---|
| 873 | | def isrelatedtype(self, pytype1, pytype2): |
|---|
| 874 | | """If values of both types are expressed with the same SQL, return True.""" |
|---|
| 875 | | if issubclass(pytype1, pytype2) or issubclass(pytype2, pytype1): |
|---|
| 876 | | return True |
|---|
| 877 | | if issubclass(pytype1, basestring) and issubclass(pytype2, basestring): |
|---|
| 878 | | return True |
|---|
| 879 | | if ((issubclass(pytype1, int) or issubclass(pytype1, long)) and |
|---|
| 880 | | (issubclass(pytype2, int) or issubclass(pytype2, long))): |
|---|
| 881 | | return True |
|---|
| 882 | | if typerefs.fixedpoint: |
|---|
| 883 | | if typerefs.decimal: |
|---|
| 884 | | if ((issubclass(pytype1, typerefs.fixedpoint.FixedPoint) |
|---|
| 885 | | or issubclass(pytype1, typerefs.decimal.Decimal)) and |
|---|
| 886 | | (issubclass(pytype2, typerefs.fixedpoint.FixedPoint) |
|---|
| 887 | | or issubclass(pytype2, typerefs.decimal.Decimal))): |
|---|
| 888 | | return True |
|---|
| 889 | | else: |
|---|
| 890 | | if (issubclass(pytype1, typerefs.fixedpoint.FixedPoint) and |
|---|
| 891 | | issubclass(pytype2, typerefs.fixedpoint.FixedPoint)): |
|---|
| 892 | | return True |
|---|
| 893 | | else: |
|---|
| 894 | | if typerefs.decimal: |
|---|
| 895 | | if (issubclass(pytype1, typerefs.decimal.Decimal) and |
|---|
| 896 | | issubclass(pytype2, typerefs.decimal.Decimal)): |
|---|
| 897 | | return True |
|---|
| 898 | | return False |
|---|
| 899 | | |
|---|
| 900 | | # Naming # |
|---|
| 901 | | |
|---|
| 902 | | sql_name_max_length = 64 |
|---|
| 903 | | sql_name_caseless = False |
|---|
| 904 | | |
|---|
| 905 | | def quote(self, name): |
|---|
| 906 | | """Return name, quoted for use in an SQL statement.""" |
|---|
| 907 | | # This base class doesn't use "quote", |
|---|
| 908 | | # but most subclasses will. |
|---|
| 909 | | return name |
|---|
| 910 | | |
|---|
| 911 | | def sql_name(self, key): |
|---|
| 912 | | """Return the native SQL version of key.""" |
|---|
| 913 | | if self.sql_name_caseless: |
|---|
| 914 | | key = key.lower() |
|---|
| 915 | | |
|---|
| 916 | | maxlen = self.sql_name_max_length |
|---|
| 917 | | if maxlen and len(key) > maxlen: |
|---|
| 918 | | errors.warn("The name '%s' is longer than the maximum of " |
|---|
| 919 | | "%s characters." % (key, maxlen)) |
|---|
| 920 | | key = key[:maxlen] |
|---|
| 921 | | |
|---|
| 922 | | return key |
|---|
| 923 | | |
|---|
| 924 | | def is_timeout_error(self, exc): |
|---|
| 925 | | """If the given exception instance is a lock timeout, return True. |
|---|
| 926 | | |
|---|
| 927 | | This should return True for errors which arise from locking |
|---|
| 928 | | timeouts; for example, if the database prevents 'dirty reads' |
|---|
| 929 | | by raising an error. |
|---|
| 930 | | """ |
|---|
| 931 | | # You should definitely override this for your database. |
|---|
| 932 | | return False |
|---|
| 933 | | |
|---|
| 934 | | def execute(self, query, conn=None): |
|---|
| 935 | | """Return a native response for the given query.""" |
|---|
| 936 | | if conn is None: |
|---|
| 937 | | conn = self.connections.get() |
|---|
| 938 | | if isinstance(query, unicode): |
|---|
| 939 | | query = query.encode(self.adaptertosql.encoding) |
|---|
| 940 | | self.log(query) |
|---|
| 941 | | return conn.query(query) |
|---|
| 942 | | |
|---|
| 943 | | def execute_ddl(self, query, conn=None): |
|---|
| 944 | | """Return a native response for the given DDL statement. |
|---|
| 945 | | |
|---|
| 946 | | In general, DDL statements should lock out other statements |
|---|
| 947 | | (especially those isolated in other transactions). Use this |
|---|
| 948 | | method to perform a locked DDL statement. |
|---|
| 949 | | """ |
|---|
| 950 | | self.connections.lock("Transaction denied due to DDL: %r" % query) |
|---|
| 951 | | try: |
|---|
| 952 | | ## # Must shut down all connections to avoid |
|---|
| 953 | | ## # "being accessed by other users" error? |
|---|
| 954 | | ## self.connections.shutdown() |
|---|
| 955 | | if conn is None: |
|---|
| 956 | | # Important: use _factory(), not get(), to avoid the lock |
|---|
| 957 | | conn = self.connections._factory() |
|---|
| 958 | | self.execute(query, conn) |
|---|
| 959 | | finally: |
|---|
| 960 | | self.connections.unlock() |
|---|
| 961 | | |
|---|
| 962 | | def fetch(self, query, conn=None): |
|---|
| 963 | | """Return rowdata, columns (name, type) for the given query. |
|---|
| 964 | | |
|---|
| 965 | | query should be a SQL query in string format |
|---|
| 966 | | rowdata will be an iterable of iterables containing the result values. |
|---|
| 967 | | columns will be an iterable of (column name, data type) pairs. |
|---|
| 968 | | |
|---|
| 969 | | This base class uses _sqlite syntax. |
|---|
| 970 | | """ |
|---|
| 971 | | res = self.execute(query, conn) |
|---|
| 972 | | return res.row_list, res.col_defs |
|---|
| 973 | | |
|---|
| 974 | | def select(self, relation, attributes, restriction=None, distinct=False): |
|---|
| 975 | | """Yield matching data, coerced to Python types (where known).""" |
|---|
| 976 | | sel = self.selectwriter(self, relation, attributes, restriction) |
|---|
| 977 | | data, _ = self.fetch(sel.sql(distinct)) |
|---|
| 978 | | return ResultSet(data, sel.columns, sel.imperfect) |
|---|
| 979 | | |
|---|
| 980 | | def schema(self, name): |
|---|
| 981 | | return self.schemaclass(self, name) |
|---|
| 982 | | |
|---|
| 983 | | |
|---|
| 984 | | class ResultSet: |
|---|
| 985 | | |
|---|
| 986 | | def __init__(self, data, columns, imperfect): |
|---|
| 987 | | self.data = data |
|---|
| 988 | | self.columns = columns |
|---|
| 989 | | self.imperfect = imperfect |
|---|
| 990 | | self.cursor = 0 |
|---|
| 991 | | |
|---|
| 992 | | def __iter__(self): |
|---|
| 993 | | return self |
|---|
| 994 | | |
|---|
| 995 | | def next(self): |
|---|
| 996 | | try: |
|---|
| 997 | | row = self.data[self.cursor] |
|---|
| 998 | | self.cursor += 1 |
|---|
| 999 | | except IndexError: |
|---|
| 1000 | | raise StopIteration |
|---|
| 1001 | | |
|---|
| 1002 | | coerced_row = [] |
|---|
| 1003 | | for i, (table, col, qname) in enumerate(self.columns): |
|---|
| 1004 | | val = row[i] |
|---|
| 1005 | | if table and col: |
|---|
| 1006 | | val = table.schema.db.adapterfromdb.coerce(val, col.dbtype, col.pytype) |
|---|
| 1007 | | coerced_row.append(val) |
|---|
| 1008 | | return coerced_row |
|---|
| 1009 | | |
|---|