|
10 | 10 | import struct
|
11 | 11 | import time
|
12 | 12 |
|
| 13 | +from . import compat |
13 | 14 | from . import connect_utils
|
14 | 15 | from . import cursor
|
15 | 16 | from . import exceptions
|
|
18 | 19 | from . import protocol
|
19 | 20 | from . import serverversion
|
20 | 21 | from . import transaction
|
| 22 | +from . import utils |
21 | 23 |
|
22 | 24 |
|
23 | 25 | class ConnectionMeta(type):
|
@@ -345,6 +347,178 @@ async def fetchrow(self, query, *args, timeout=None):
|
345 | 347 | return None
|
346 | 348 | return data[0]
|
347 | 349 |
|
| 350 | + async def copy_from_table(self, table_name, *, output, |
| 351 | + columns=None, schema_name=None, timeout=None, |
| 352 | + format=None, oids=None, delimiter=None, |
| 353 | + null=None, header=None, quote=None, |
| 354 | + escape=None, force_quote=None, encoding=None): |
| 355 | + """Copy table contents to a file or file-like object. |
| 356 | +
|
| 357 | + :param str table_name: |
| 358 | + The name of the table to copy data from. |
| 359 | +
|
| 360 | + :param output: |
| 361 | + A :term:`path-like object <python:path-like object>`, |
| 362 | + or a :term:`file-like object <python:file-like object>`, or |
| 363 | + a :term:`coroutine function <python:coroutine function>` |
| 364 | + that takes a ``bytes`` instance as a sole argument. |
| 365 | +
|
| 366 | + :param list columns: |
| 367 | + An optional list of column names to copy. |
| 368 | +
|
| 369 | + :param str schema_name: |
| 370 | + An optional schema name to qualify the table. |
| 371 | +
|
| 372 | + :param float timeout: |
| 373 | + Optional timeout value in seconds. |
| 374 | +
|
| 375 | + The remaining kewyword arguments are ``COPY`` statement options, |
| 376 | + see `COPY statement documentation`_ for details. |
| 377 | +
|
| 378 | + :return: The status string of the COPY command. |
| 379 | +
|
| 380 | + .. versionadded:: 0.11.0 |
| 381 | +
|
| 382 | + .. _`COPY statement documentation`: https://www.postgresql.org/docs/\ |
| 383 | + current/static/sql-copy.html |
| 384 | +
|
| 385 | + """ |
| 386 | + tabname = utils._quote_ident(table_name) |
| 387 | + if schema_name: |
| 388 | + tabname = utils._quote_ident(schema_name) + '.' + tabname |
| 389 | + |
| 390 | + if columns: |
| 391 | + cols = '({})'.format( |
| 392 | + ', '.join(utils._quote_ident(c) for c in columns)) |
| 393 | + else: |
| 394 | + cols = '' |
| 395 | + |
| 396 | + opts = self._format_copy_opts( |
| 397 | + format=format, oids=oids, delimiter=delimiter, |
| 398 | + null=null, header=header, quote=quote, escape=escape, |
| 399 | + force_quote=force_quote, encoding=encoding |
| 400 | + ) |
| 401 | + |
| 402 | + copy_stmt = 'COPY {tab}{cols} TO STDOUT {opts}'.format( |
| 403 | + tab=tabname, cols=cols, opts=opts) |
| 404 | + |
| 405 | + return await self._copy_out(copy_stmt, output, timeout) |
| 406 | + |
| 407 | + async def copy_from_query(self, query, *args, output, |
| 408 | + timeout=None, format=None, oids=None, |
| 409 | + delimiter=None, null=None, header=None, |
| 410 | + quote=None, escape=None, force_quote=None, |
| 411 | + encoding=None): |
| 412 | + """Copy the results of a query to a file or file-like object. |
| 413 | +
|
| 414 | + :param str query: |
| 415 | + The query to copy the results of. |
| 416 | +
|
| 417 | + :param *args: |
| 418 | + Query arguments. |
| 419 | +
|
| 420 | + :param output: |
| 421 | + A :term:`path-like object <python:path-like object>`, |
| 422 | + or a :term:`file-like object <python:file-like object>`, or |
| 423 | + a :term:`coroutine function <python:coroutine function>` |
| 424 | + that takes a ``bytes`` instance as a sole argument. |
| 425 | +
|
| 426 | + :param float timeout: |
| 427 | + Optional timeout value in seconds. |
| 428 | +
|
| 429 | + The remaining kewyword arguments are ``COPY`` statement options, |
| 430 | + see `COPY statement documentation`_ for details. |
| 431 | +
|
| 432 | + :return: The status string of the COPY command. |
| 433 | +
|
| 434 | + .. versionadded:: 0.11.0 |
| 435 | +
|
| 436 | + .. _`COPY statement documentation`: https://www.postgresql.org/docs/\ |
| 437 | + current/static/sql-copy.html |
| 438 | +
|
| 439 | + """ |
| 440 | + opts = self._format_copy_opts( |
| 441 | + format=format, oids=oids, delimiter=delimiter, |
| 442 | + null=null, header=header, quote=quote, escape=escape, |
| 443 | + force_quote=force_quote, encoding=encoding |
| 444 | + ) |
| 445 | + |
| 446 | + if args: |
| 447 | + query = await utils._mogrify(self, query, args) |
| 448 | + |
| 449 | + copy_stmt = 'COPY ({query}) TO STDOUT {opts}'.format( |
| 450 | + query=query, opts=opts) |
| 451 | + |
| 452 | + return await self._copy_out(copy_stmt, output, timeout) |
| 453 | + |
| 454 | + def _format_copy_opts(self, *, format=None, oids=None, freeze=None, |
| 455 | + delimiter=None, null=None, header=None, quote=None, |
| 456 | + escape=None, force_quote=None, force_not_null=None, |
| 457 | + force_null=None, encoding=None): |
| 458 | + kwargs = dict(locals()) |
| 459 | + kwargs.pop('self') |
| 460 | + opts = [] |
| 461 | + |
| 462 | + if force_quote is not None and isinstance(force_quote, bool): |
| 463 | + kwargs.pop('force_quote') |
| 464 | + if force_quote: |
| 465 | + opts.append('FORCE_QUOTE *') |
| 466 | + |
| 467 | + for k, v in kwargs.items(): |
| 468 | + if v is not None: |
| 469 | + if k in ('force_not_null', 'force_null', 'force_quote'): |
| 470 | + v = '(' + ', '.join(utils._quote_ident(c) for c in v) + ')' |
| 471 | + elif k in ('oids', 'freeze', 'header'): |
| 472 | + v = str(v) |
| 473 | + else: |
| 474 | + v = utils._quote_literal(v) |
| 475 | + |
| 476 | + opts.append('{} {}'.format(k.upper(), v)) |
| 477 | + |
| 478 | + if opts: |
| 479 | + return '(' + ', '.join(opts) + ')' |
| 480 | + else: |
| 481 | + return '' |
| 482 | + |
| 483 | + async def _copy_out(self, copy_stmt, output, timeout): |
| 484 | + try: |
| 485 | + path = compat.fspath(output) |
| 486 | + except TypeError: |
| 487 | + # output is not a path-like object |
| 488 | + path = None |
| 489 | + |
| 490 | + writer = None |
| 491 | + opened_by_us = False |
| 492 | + run_in_executor = self._loop.run_in_executor |
| 493 | + |
| 494 | + if path is not None: |
| 495 | + # a path |
| 496 | + f = await run_in_executor(None, open, path, 'wb') |
| 497 | + opened_by_us = True |
| 498 | + elif hasattr(output, 'write'): |
| 499 | + # file-like |
| 500 | + f = output |
| 501 | + elif callable(output): |
| 502 | + # assuming calling output returns an awaitable. |
| 503 | + writer = output |
| 504 | + else: |
| 505 | + raise TypeError( |
| 506 | + 'output is expected to be a file-like object, ' |
| 507 | + 'a path-like object or a coroutine function, ' |
| 508 | + 'not {}'.format(type(output).__name__) |
| 509 | + ) |
| 510 | + |
| 511 | + if writer is None: |
| 512 | + async def _writer(data): |
| 513 | + await run_in_executor(None, f.write, data) |
| 514 | + writer = _writer |
| 515 | + |
| 516 | + try: |
| 517 | + return await self._protocol.copy_out(copy_stmt, writer, timeout) |
| 518 | + finally: |
| 519 | + if opened_by_us: |
| 520 | + f.close() |
| 521 | + |
348 | 522 | async def set_type_codec(self, typename, *,
|
349 | 523 | schema='public', encoder, decoder, binary=False):
|
350 | 524 | """Set an encoder/decoder pair for the specified data type.
|
|
0 commit comments