diff --git a/examples/quickstart-tuto.py b/examples/quickstart-tuto.py index 19f9b60cbbe558acb8691a386b194410fb53a0c4..810c2f2b5ea00f09c49c34147321dd3ed9382b8c 100644 --- a/examples/quickstart-tuto.py +++ b/examples/quickstart-tuto.py @@ -1,4 +1,4 @@ -from querybuilder.schemas import table, schema +from querybuilder.schemas.helper import table, schema from querybuilder.queries.algebra.columns import ( Value, Constant, diff --git a/querybuilder/queries/algebra/columns.py b/querybuilder/queries/algebra/columns.py index 04ce32cc6fc9ecde0fdd42fd0c82c245c9aadd09..1d1f959afd79613b18097fe25ee9158af0ebde29 100644 --- a/querybuilder/queries/algebra/columns.py +++ b/querybuilder/queries/algebra/columns.py @@ -109,14 +109,14 @@ def name_column( schema_name: Optional[str] = None, preserve_table_column: bool = False, ) -> Column: + if not name: + name = getattr(column, "name", None) + if preserve_table_column and isinstance(column, TableColumn): return column.buildfrom( - column, relation_name=relation_name, schema_name=schema_name + column, name=name, relation_name=relation_name, schema_name=schema_name ) - if not name: - name = getattr(column, "name", None) - if name: return Named( sqltype=column.sqltype, diff --git a/querybuilder/queries/algebra/relations.py b/querybuilder/queries/algebra/relations.py index b64ba8afb5e1f49f01176debf748ac02d8133500..9813bc7d85fac3bdbf1bc7e0d6a41d3a51735f8d 100644 --- a/querybuilder/queries/algebra/relations.py +++ b/querybuilder/queries/algebra/relations.py @@ -126,7 +126,7 @@ class Fromable(Relation): # assert isinstance(c, (int, str)) columns_to_select.append(self.columns.resolve(c)) return querybuilder.queries.dql.Select( - from_=self, columns=columns_to_select, **kwargs + from_=self, selected_columns=columns_to_select, **kwargs ) @method_accepting_lambdas diff --git a/querybuilder/queries/dql.py b/querybuilder/queries/dql.py index 0f055a6df7934fe388c4249e578c6984fa54e4ca..50b1050ece2f380be68f3f497c1188cab216479d 100644 --- a/querybuilder/queries/dql.py +++ b/querybuilder/queries/dql.py @@ -151,7 +151,7 @@ class Values(DQLQuery): class Select(DQLQuery): __slots__: tuple[str, ...] = ( - "column_origins", + "selected_columns", "aliases", "from_", "where", @@ -167,7 +167,7 @@ class Select(DQLQuery): def __init__( self, - columns: Sequence[qbcolumns.Column | qbcolumns.Star], + selected_columns: Sequence[qbcolumns.Column | qbcolumns.Star], aliases: Mapping[int, str] | Iterable[tuple[int, str]] = (), distinct: Optional[qbcolumns.Column | bool] = None, from_: Optional[qbrelations.Fromable] = None, @@ -191,12 +191,12 @@ class Select(DQLQuery): self.offset = offset self.from_ = from_ - super().__init__(columns=columns, **kwargs) + super().__init__(columns=selected_columns, **kwargs) @property def columns(self): columns = [] - for i, c in enumerate(self.column_origins): + for i, c in enumerate(self.selected_columns): if isinstance(c, qbcolumns.Star): columns.extend(self.from_.columns) else: @@ -208,19 +208,21 @@ class Select(DQLQuery): return self._column_store_factory(columns) - def _init_columns(self, columns): + def _init_columns(self, selected_columns): for k in self.aliases.keys(): - if 0 <= k < len(columns) and isinstance(columns[k], qbcolumns.Star): + if 0 <= k < len(selected_columns) and isinstance( + selected_columns[k], qbcolumns.Star + ): raise KeyError("Cannot alias a * column") - for c in columns: + for c in selected_columns: if isinstance(c, qbcolumns.Star): assert self.from_ # TODO: Maybe another exception would be better break - self.column_origins = self._column_store_factory(columns) + self.selected_columns = self._column_store_factory(selected_columns) def accept(self, visitor, /): - for col in self.column_origins: + for col in self.selected_columns: visitor = col.accept(visitor) for res in (self.from_, self.where, self.groupby, self.having, self.orderby): if res: @@ -230,7 +232,7 @@ class Select(DQLQuery): def descend(self, accumulator): accumulator = super().descend(accumulator) - for ocol in self.column_origins: + for ocol in self.selected_columns: accumulator = ocol.descend(accumulator) for res in (self.from_, self.groupby, self.having, self.orderby, self.where): @@ -240,16 +242,20 @@ class Select(DQLQuery): return accumulator def set_selected_columns( - self, columns: Iterable[str | int | qbcolumns.Column | qbcolumns.Star], / + self, + selected_columns: Iterable[str | int | qbcolumns.Column | qbcolumns.Star], + /, ) -> Select: """Method for resetting selected columns of the query + Aliases are not preserved and Star is allowed. + Parameters ---------- - columns: Iterable[str | int | qbcolumns.Column | qbcolumns.Star] + selected_columns: Iterable[str | int | qbcolumns.Column | qbcolumns.Star] the specifications of the columns to select. Each specification might be an index (int), a name (str), an explicit column, or the special operator Star. - In the two first cases, it is resolved within the `column_origins` store, + In the two first cases, it is resolved within the `selected_columns` store, using the `resolve` method. You may want to consider using `set_columns` for setting columns using similar specifications that are resolved within the `columns` store context instead. A side effect of the method is that all @@ -276,22 +282,35 @@ class Select(DQLQuery): 'SELECT t.y, 2, t.x FROM t' Indeed, in the list parameter `[2, Constant(int, 2), "x"]`, `2` stands for the - origin column of index 2 (ie, `sq.column_origins[2]`), and `"x"` stands for the - origin column that is unambiguously named "x" (ie, `sq.column_origins._["x"]`). + selected column of index 2 (ie, `sq.selected_columns[2]`), and `"x"` stands for the + selected column that is unambiguously named "x" (ie, `sq.selected_columns._["x"]`). + + It is also possible to use `querybuilder.queries.algebra.columns.Star()`, or the + `"*"` shorthand, in the sequence of selected columns. + >>> from querybuilder.queries.algebra.columns import Star + >>> str(sq.set_selected_columns(["*", 2, Star(), "x"])) + 'SELECT *, t.y, *, t.x FROM t' + """ cols: list[qbcolumns.Column | qbcolumns.Star] = [] - for c in columns: + for c in selected_columns: if c == "*": + assert self.from_ cols.append(qbcolumns.Star()) - elif isinstance(c, (qbcolumns.Column, qbcolumns.Star)): + elif isinstance(c, qbcolumns.Star): + assert self.from_ + cols.append(c) + elif isinstance(c, qbcolumns.Column): cols.append(c) else: # assert isinstance(c, (int, str)) - cols.append(self.column_origins.resolve(c)) + cols.append(self.selected_columns.resolve(c)) return self.set_aliases(())._set_columns(cols) def set_columns(self, columns: Iterable[str | int | qbcolumns.Column], /) -> Select: - """Method for resetting exposed (thus selected) columns of the query + """Method for resetting exposed columns of the query + + Aliases are kept; Star is forbidden. Parameters ---------- @@ -301,7 +320,7 @@ class Select(DQLQuery): is resolved within the `columns` store, using the `resolve` method. If the corresponding column was aliased, then the alias is kept. The special Star operator is not allowed here, since the specifications are resolved according - to the exposed columns, and not their origins. + to the exposed columns, and not their selected origins. Related methods --------------- @@ -344,8 +363,9 @@ class Select(DQLQuery): else: orig_c = c index = None + if index is not None: - orig_c = self.column_origins[index] + orig_c = self.selected_columns[index] alias = self.aliases.get(index) if alias: aliases[i] = alias @@ -426,7 +446,7 @@ class Select(DQLQuery): clauses.AliasedColumn(c, self.aliases.get(i, None)).subtokenize(tokenizer) if isinstance(c, qbcolumns.Column) else c.subtokenize(tokenizer) - for i, c in enumerate(self.column_origins) + for i, c in enumerate(self.selected_columns) ) if self.distinct: add_kwargs["distinct"] = clauses.DistinctColumn(self.distinct).subtokenize( @@ -469,16 +489,12 @@ class Select(DQLQuery): return self.buildfrom(self, columns=columns) def _post_getstate(self, state): - state["columns"] = state.pop("column_origins") + state["columns"] = state.pop("selected_columns") return state def _substitute(self, substitutions: Mapping): - columns = map( - operator.methodcaller("substitute", substitutions), - self.column_origins, - ) - - d = dict(columns=list(columns)) + columns = [c.substitute(substitutions) for c in self.selected_columns] + d = dict(columns=columns) for k in ("where", "distinct", "groupby", "orderby", "having", "from_"): res = getattr(self, k) diff --git a/querybuilder/tests/queries/test_dql.py b/querybuilder/tests/queries/test_dql.py index d8296f2ab4a93fbf182e7a956df7b38e8a8a7180..7b4ed1999639f55db5457b230ffc8011dc920de0 100644 --- a/querybuilder/tests/queries/test_dql.py +++ b/querybuilder/tests/queries/test_dql.py @@ -42,7 +42,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_columns_unaliased_columns_with_list_of_strings(self): @@ -55,7 +55,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_columns_unaliased_columns_with_list_of_columns(self): @@ -68,7 +68,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_columns_unaliased_columns_with_list_of_mixed_specifications(self): @@ -88,7 +88,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_columns.append(qbcolumns.Constant(int, 3)) - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_columns_aliased_columns_with_list_of_integers(self): @@ -103,7 +103,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_aliases = {0: "a0", 1: "a4", 3: "a0"} - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert expected_aliases == proj.aliases def test_set_columns_aliased_columns_with_list_of_strings(self): @@ -118,7 +118,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_aliases = {0: "a0", 1: "a4", 3: "a0"} - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert expected_aliases == proj.aliases def test_set_columns_aliased_columns_with_list_of_columns(self): @@ -134,7 +134,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_aliases = {0: "a0", 1: "a4", 3: "a0"} - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert expected_aliases == proj.aliases def test_set_columns_aliased_columns_with_list_of_mixed_specifications(self): @@ -151,7 +151,7 @@ class TestSelect(TestDQL): expected_columns.append(qbcolumns.Constant(int, 3)) expected_aliases = {0: "a0", 1: "a4", 3: "a0"} - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert expected_aliases == proj.aliases def test_set_selected_columns_unaliased_columns_with_list_of_integers(self): @@ -164,7 +164,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_selected_columns_unaliased_columns_with_list_of_strings(self): @@ -177,7 +177,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_selected_columns_unaliased_columns_with_list_of_columns(self): @@ -190,7 +190,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_selected_columns_unaliased_columns_with_list_of_mixed_specifications( @@ -212,7 +212,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_columns.append(qbcolumns.Constant(int, 3)) - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert {} == proj.aliases def test_set_selected_columns_aliased_columns_with_list_of_integers(self): @@ -226,7 +226,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert not proj.aliases def test_set_selected_columns_aliased_columns_with_list_of_strings(self): @@ -240,7 +240,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert not proj.aliases def test_set_selected_columns_aliased_columns_with_list_of_columns(self): @@ -255,7 +255,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert not proj.aliases def test_set_selected_columns_aliased_columns_with_list_of_mixed_specifications( @@ -273,7 +273,7 @@ class TestSelect(TestDQL): expected_columns = [qbcolumns.Named(int, f"c{i}") for i in proj_indices] expected_columns.append(qbcolumns.Constant(int, 3)) - assert expected_columns == list(proj.column_origins) + assert expected_columns == list(proj.selected_columns) assert not proj.aliases def test_column_property(self):