Skip to content

Commit d4bd054

Browse files
KangOlaj-fuentes
andcommitted
[IMP] util/pg: allow SQL formatting of column lists
With optional leading and trailing commas. Also allow to specify the table alias to use. closes #20 Signed-off-by: Christophe Simonis (chs) <[email protected]> Co-authored-by: Alvaro Fuentes <[email protected]>
1 parent 2182e7c commit d4bd054

File tree

2 files changed

+100
-3
lines changed

2 files changed

+100
-3
lines changed

src/base/tests/test_util.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,3 +977,38 @@ class TestQueryFormat(UnitTestCase):
977977
def test_format(self, query, args, kwargs, expected):
978978
cr = self.env.cr
979979
self.assertEqual(util.format_query(cr, query, *args, **kwargs), expected)
980+
981+
def test_format_ColumnList(self):
982+
cr = self.env.cr
983+
984+
ignored = ("id", "create_date", "create_uid", "write_date", "write_uid")
985+
986+
columns = util.get_columns(cr, "ir_config_parameter", ignore=ignored)
987+
no_columns = util.get_columns(cr, "ir_config_parameter", ignore=(*ignored, "key", "value"))
988+
989+
self.assertEqual(
990+
util.format_query(cr, "SELECT id, {c}", c=columns),
991+
'SELECT id, "key", "value"',
992+
)
993+
994+
self.assertEqual(
995+
util.format_query(cr, "SELECT id {c}", c=columns.using(leading_comma=True)),
996+
'SELECT id , "key", "value"',
997+
)
998+
self.assertEqual(
999+
util.format_query(cr, "SELECT {c} id", c=columns.using(trailing_comma=True)),
1000+
'SELECT "key", "value", id',
1001+
)
1002+
self.assertEqual(
1003+
util.format_query(cr, "SELECT {c}", c=columns.using(alias="a")),
1004+
'SELECT "a"."key", "a"."value"',
1005+
)
1006+
# leading/trailing comma only if list is not empty
1007+
self.assertEqual(
1008+
util.format_query(cr, "SELECT id {c}", c=no_columns.using(leading_comma=True)),
1009+
"SELECT id ",
1010+
)
1011+
self.assertEqual(
1012+
util.format_query(cr, "SELECT {c} id", c=no_columns.using(trailing_comma=True)),
1013+
"SELECT id",
1014+
)

src/util/pg.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77
import uuid
88
import warnings
99
from contextlib import contextmanager
10-
from functools import reduce
10+
from functools import partial, reduce
1111
from multiprocessing import cpu_count
1212

1313
try:
1414
from concurrent.futures import ThreadPoolExecutor
1515
except ImportError:
1616
ThreadPoolExecutor = None
1717

18+
try:
19+
from collections import UserList
20+
except ImportError:
21+
from UserList import UserList
22+
1823
import psycopg2
1924
from psycopg2 import sql
2025

@@ -670,21 +675,78 @@ def get_depending_views(cr, table, column):
670675
return cr.fetchall()
671676

672677

678+
class ColumnList(UserList, sql.Composable):
679+
"""
680+
Encapsulate a list of elements that represent column names.
681+
The resulting object can be rendered as string with leading/trailing comma or an alias.
682+
683+
Examples:
684+
```
685+
>>> columns = ColumnList(["id", "field_Yx"], ["id", '"field_Yx"'])
686+
687+
>>> columns.using(alias="t").as_string(cr._cnx)
688+
'"t"."id", "t"."field_Yx"'
689+
690+
>>> columns.using(leading_comma=True).as_string(cr._cnx)
691+
', "id", "field_Yx"'
692+
693+
>>> util.format(cr, "SELECT {} t.name FROM table t", columns.using(alias="t", trailing_comma=True))
694+
'SELECT "t"."id", "t"."field_Yx", t.name FROM table t'
695+
```
696+
"""
697+
698+
def __init__(self, list_=(), quoted=()):
699+
assert len(list_) == len(quoted)
700+
self._unquoted_columns = list(list_)
701+
super(ColumnList, self).__init__(quoted)
702+
self._leading_comma = False
703+
self._trailing_comma = False
704+
self._alias = None
705+
706+
def using(self, leading_comma=False, trailing_comma=False, alias=None):
707+
if self._leading_comma is leading_comma and self._trailing_comma is trailing_comma and self._alias == alias:
708+
return self
709+
new = ColumnList(self._unquoted_columns, self.data)
710+
new._leading_comma = leading_comma
711+
new._trailing_comma = trailing_comma
712+
new._alias = alias
713+
return new
714+
715+
def as_string(self, context):
716+
head = sql.SQL(", " if self._leading_comma and self else "")
717+
tail = sql.SQL("," if self._trailing_comma and self else "")
718+
719+
if not self._alias:
720+
builder = sql.Identifier
721+
elif hasattr(sql.Identifier, "strings"):
722+
builder = partial(sql.Identifier, self._alias)
723+
else:
724+
# older psycopg2 versions, doesn't support passing multiple strings to the constructor
725+
builder = lambda elem: sql.SQL(".").join(sql.Identifier(self._alias) + sql.Identifier(elem))
726+
727+
body = sql.SQL(", ").join(builder(elem) for elem in self._unquoted_columns)
728+
return sql.Composed([head, body, tail]).as_string(context)
729+
730+
def iter_unquoted(self):
731+
return iter(self._unquoted_columns)
732+
733+
673734
def get_columns(cr, table, ignore=("id",)):
674735
"""return the list of columns in table (minus ignored ones)"""
675736
_validate_table(table)
676737

677738
cr.execute(
678739
"""
679-
SELECT quote_ident(column_name)
740+
SELECT coalesce(array_agg(column_name::varchar ORDER BY column_name), ARRAY[]::varchar[]),
741+
coalesce(array_agg(quote_ident(column_name) ORDER BY column_name), ARRAY[]::varchar[])
680742
FROM information_schema.columns
681743
WHERE table_schema = 'public'
682744
AND table_name = %s
683745
AND column_name != ALL(%s)
684746
""",
685747
[table, list(ignore)],
686748
)
687-
return [c for c, in cr.fetchall()]
749+
return ColumnList(*cr.fetchone())
688750

689751

690752
def rename_table(cr, old_table, new_table, remove_constraints=True):

0 commit comments

Comments
 (0)