From c61e968395cb5916f24ed472f1c1d0581ed252f6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 10:32:25 +0500 Subject: [PATCH 01/83] update setup files --- setup.cfg | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 00aca2e..625a08d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = +tag_build = 0.3.1 tag_date = 0 diff --git a/setup.py b/setup.py index dcb65f8..5f1c2c0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ :copyright: (c) 2022 co0lc0der """ -version = '0.3.0' +version = '0.3.1' with open('README.md', encoding='utf-8') as f: long_description = f.read() @@ -37,7 +37,6 @@ license='MIT, see LICENSE.md file', packages=['simple-query-builder'], - install_requires=['sqlite3', 'Union'], classifiers=[ 'License :: OSI Approved :: MIT License', From 94b9a25b46cce6aef235f32a7757d1c86875ff67 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 11:31:28 +0500 Subject: [PATCH 02/83] update README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cdea7eb..6d5c0cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # QueryBuilder python module -![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=orange) ![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet) ![GitHub Pull Requests](https://img.shields.io/github/issues-pr/co0lc0der/simple-query-builder-python?color=blueviolet) ![License](https://img.shields.io/pypi/l/simple-query-builder?color=blueviolet) ![Forks](https://img.shields.io/github/forks/co0lc0der/simple-query-builder-python?style=social) +![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) +[![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) +![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) +![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) +[![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) This is a small easy-to-use component for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). @@ -10,7 +14,7 @@ Bug reports and/or pull requests are welcome ## License -The module is available as open source under the terms of the [MIT](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) +The module is available as open source under the terms of the [MIT license](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) ## Installation From 1e2a33d7c0bfcd7550a232f6594d5bd909f47fc4 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 11:31:56 +0500 Subject: [PATCH 03/83] update module version --- example/example.py | 0 setup.py | 7 +++++-- simple-query-builder/__init__.py | 4 ++-- simple-query-builder/querybuilder.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 example/example.py diff --git a/example/example.py b/example/example.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 5f1c2c0..d9e8e02 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ """ :authors: co0lc0der -:license: MIT, see LICENSE file +:license: MIT :copyright: (c) 2022 co0lc0der """ @@ -34,11 +34,13 @@ version ), - license='MIT, see LICENSE.md file', + license='MIT', packages=['simple-query-builder'], classifiers=[ + 'Topic :: Database', + 'Topic :: Database :: Database Engines/Servers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Intended Audience :: End Users/Desktop', @@ -51,5 +53,6 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: SQL' ] ) diff --git a/simple-query-builder/__init__.py b/simple-query-builder/__init__.py index ff33c44..31b8471 100644 --- a/simple-query-builder/__init__.py +++ b/simple-query-builder/__init__.py @@ -1,11 +1,11 @@ """ :authors: co0lc0der -:license: MIT, see LICENSE file +:license: MIT :copyright: (c) 2022 co0lc0der """ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.0' +__version__ = '0.3.1' __email__ = 'c0der@ya.ru' diff --git a/simple-query-builder/querybuilder.py b/simple-query-builder/querybuilder.py index 53bf019..53af9f4 100644 --- a/simple-query-builder/querybuilder.py +++ b/simple-query-builder/querybuilder.py @@ -1,6 +1,6 @@ """ :authors: co0lc0der -:license: MIT License, see LICENSE file +:license: MIT :copyright: (c) 2022 co0lc0der """ From 5813e1b740b0dd6355d69df87565e2c0f0988d07 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 12:00:17 +0500 Subject: [PATCH 04/83] rename module --- README.md | 8 ++++---- setup.py | 6 +++--- .../__init__.py | 2 +- .../querybuilder.py | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename {simple-query-builder => simple_query_builder}/__init__.py (87%) rename {simple-query-builder => simple_query_builder}/querybuilder.py (100%) diff --git a/README.md b/README.md index 6d5c0cd..dbd4654 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # QueryBuilder python module -![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) +![PyPI](https://img.shields.io/pypi/v/simple_query_builder?color=yellow&style=flat-square) [![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) ![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) -![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) +![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple_query_builder?color=blueviolet&style=flat-square) [![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) This is a small easy-to-use component for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). @@ -21,7 +21,7 @@ The module is available as open source under the terms of the [MIT license](http Install the current version with [PyPI](https://pypi.org/project/simple-query-builder): ```bash -pip install simple-query-builder +pip install simple_query_builder ``` Or from Github: @@ -50,7 +50,7 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai ### Import the module and init `QueryBuilder` with `Database()` ```python -from querybuilder import * +from simple_query_builder import * qb = QueryBuilder(DataBase(), 'my_db.db') ``` diff --git a/setup.py b/setup.py index d9e8e02..863a219 100644 --- a/setup.py +++ b/setup.py @@ -9,13 +9,13 @@ :copyright: (c) 2022 co0lc0der """ -version = '0.3.1' +version = '0.3.2' with open('README.md', encoding='utf-8') as f: long_description = f.read() setup( - name='simple-query-builder', + name='simple_query_builder', version=version, author='co0lc0der', @@ -36,7 +36,7 @@ license='MIT', - packages=['simple-query-builder'], + packages=['simple_query_builder'], classifiers=[ 'Topic :: Database', diff --git a/simple-query-builder/__init__.py b/simple_query_builder/__init__.py similarity index 87% rename from simple-query-builder/__init__.py rename to simple_query_builder/__init__.py index 31b8471..47e6a1f 100644 --- a/simple-query-builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -7,5 +7,5 @@ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.1' +__version__ = '0.3.2' __email__ = 'c0der@ya.ru' diff --git a/simple-query-builder/querybuilder.py b/simple_query_builder/querybuilder.py similarity index 100% rename from simple-query-builder/querybuilder.py rename to simple_query_builder/querybuilder.py From de6526fa69e73fc70ee28b1c4348b71957156b68 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 12:00:48 +0500 Subject: [PATCH 05/83] add a small example --- example/example.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/example.py b/example/example.py index e69de29..7308479 100644 --- a/example/example.py +++ b/example/example.py @@ -0,0 +1,6 @@ +from simple_query_builder import * + +qb = QueryBuilder(DataBase(), 'my_db.db') + +res = qb.select('cabs') +print(res) From e5797c39e314555caf893c66e0bce92611c8f369 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 12:01:43 +0500 Subject: [PATCH 06/83] update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 625a08d..9438c1e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.1 +tag_build = 0.3.2 tag_date = 0 From 958d7e21c82a78e5178374fd7f4ac7e6b371542a Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 13:11:20 +0500 Subject: [PATCH 07/83] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dbd4654..0fec5be 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # QueryBuilder python module -![PyPI](https://img.shields.io/pypi/v/simple_query_builder?color=yellow&style=flat-square) +![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) [![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) ![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) -![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple_query_builder?color=blueviolet&style=flat-square) +![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) [![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) This is a small easy-to-use component for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). @@ -21,7 +21,7 @@ The module is available as open source under the terms of the [MIT license](http Install the current version with [PyPI](https://pypi.org/project/simple-query-builder): ```bash -pip install simple_query_builder +pip install simple-query-builder ``` Or from Github: From f14571f7a6691e8ddbaa507f8ac56753d1a3a266 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 13:11:31 +0500 Subject: [PATCH 08/83] update example --- example/example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/example.py b/example/example.py index 7308479..6d94bb2 100644 --- a/example/example.py +++ b/example/example.py @@ -2,5 +2,6 @@ qb = QueryBuilder(DataBase(), 'my_db.db') -res = qb.select('cabs') +res = qb.select('users').all() +# SELECT * FROM `users` print(res) From c8d1edd640aa209ce10faa5da8f9b9a8c90efbab Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 8 Sep 2022 13:17:30 +0500 Subject: [PATCH 09/83] update version --- setup.cfg | 2 +- setup.py | 2 +- simple_query_builder/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9438c1e..503a1b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.2 +tag_build = 0.3.3 tag_date = 0 diff --git a/setup.py b/setup.py index 863a219..968d345 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ :copyright: (c) 2022 co0lc0der """ -version = '0.3.2' +version = '0.3.3' with open('README.md', encoding='utf-8') as f: long_description = f.read() diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index 47e6a1f..aa6e236 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -7,5 +7,5 @@ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.2' +__version__ = '0.3.3' __email__ = 'c0der@ya.ru' From 348c46e2916ae3c400f2e6b97097128d99dbac2d Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 9 Sep 2022 17:39:33 +0500 Subject: [PATCH 10/83] update README --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0fec5be..f0cad97 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # QueryBuilder python module ![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) +![PyPI - Downloads](https://img.shields.io/pypi/dm/simple-query-builder?color=darkgreen&style=flat-square) [![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) ![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) ![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) @@ -32,19 +33,19 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai ### Main public methods - `get_sql()` returns SQL query string which will be executed - `get_params()` returns an array of parameters for a query -- `get_result()` returns query's results -- `get_count()` returns results' rows count +- `get_result()` returns query's result +- `get_count()` returns result's rows count - `get_error()` returns `True` if an error is had - `get_error_message()` returns an error message if an error is had - `set_error(message)` sets `_error` to `True` and `_error_essage` - `get_first()` returns the first item of results - `get_last()` returns the last item of results - `reset()` resets state to default values (except PDO property) -- `all()` executes SQL query and return all rows of result (`fetchall()`) -- `one()` executes SQL query and return the first row of result (`fetchone()`) -- `column(col_index)` executes SQL query and return the first column of result, `col_index` is `0` by default -- `go()` this method is for non `SELECT` queries. it executes SQL query and return nothing (but returns the last inserted row ID for `INSERT` method) -- `count()` prepares a query with SQL `COUNT()` function +- `all()` executes SQL query and returns all rows of result (`fetchall()`) +- `one()` executes SQL query and returns the first row of result (`fetchone()`) +- `column(col_index)` executes SQL query and returns the first column of result, `col_index` is `0` by default +- `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) +- `count()` prepares a query with SQL `COUNT(*)` function and executes it - `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries - 'SQL' methods are presented in [Usage section](#usage-examples) From a3e08d2e1b57fbd47f8ec1be9c82540878e79653 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sun, 18 Sep 2022 14:26:58 +0500 Subject: [PATCH 11/83] update README --- README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f0cad97..81e7858 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) [![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) -This is a small easy-to-use component for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). +This is a small easy-to-use module for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). ## Contributing @@ -72,12 +72,10 @@ SELECT * FROM `users` WHERE `id` = 10; ``` - Select rows with two conditions ```python -results = qb.select('users')\ - .where([['id', '>', 1], 'and', ['group_id', '=', 2]])\ - .all() +results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', '=', 2]]).all() ``` ```sql -SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2) +SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); ``` - Select a row with a `LIKE` and `NOT LIKE` condition ```python @@ -86,7 +84,7 @@ results = qb.select('users').like(['name', '%John%']).all() results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() ``` ```sql -SELECT * FROM `users` WHERE (`name` LIKE '%John%') +SELECT * FROM `users` WHERE (`name` LIKE '%John%'); ``` ```python results = qb.select('users').notLike(['name', '%John%']).all() @@ -94,7 +92,7 @@ results = qb.select('users').notLike(['name', '%John%']).all() results = qb.select('users').where([['name', 'NOT LIKE', '%John%']]).all() ``` ```sql -SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%') +SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%'); ``` - Select rows with `OFFSET` and `LIMIT` ```python @@ -150,7 +148,7 @@ groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM( ```sql SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` FROM `orders` WHERE (YEAR(`created_at`) = 2020) -GROUP BY `month_num` HAVING (`total` > 20000) +GROUP BY `month_num` HAVING (`total` > 20000); ``` 4. `JOIN`. Supports `INNER`, `LEFT OUTER`, `RIGHT OUTER`, `FULL OUTER` and `CROSS` joins (`INNER` is by default) ```python @@ -193,7 +191,7 @@ FROM `cabs_printers` AS `cp` INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` INNER JOIN `cartridge_types` AS `c` ON p.cartridge_id=c.id -WHERE (`cp`.`cab_id` IN (11,12,13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`) +WHERE (`cp`.`cab_id` IN (11,12,13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`); ``` - Insert a row ```python @@ -203,7 +201,7 @@ new_id = qb.insert('groups', { }).go() ``` ```sql -INSERT INTO `groups` (`name`, `permissions`) VALUES ('Moderator', 'moderator') +INSERT INTO `groups` (`name`, `permissions`) VALUES ('Moderator', 'moderator'); ``` - Insert many rows ```python @@ -219,7 +217,7 @@ INSERT INTO `groups` (`name`, `role`) VALUES ('Moderator', 'moderator'), ('Moderator2', 'moderator'), ('User', 'user'), - ('User2', 'user') + ('User2', 'user'); ``` - Update a row ```python @@ -243,7 +241,7 @@ qb.update('posts', {'status': 'published'})\ ``` ```sql UPDATE `posts` SET `status` = 'published' -WHERE (YEAR(`updated_at`) > 2020) +WHERE (YEAR(`updated_at`) > 2020); ``` - Delete a row ```python From e2ac2dc6f3c5a3cd7abb3a8263fc686b7539bcb4 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 10:27:33 +0500 Subject: [PATCH 12/83] add _prepare_sorting() method, refactor order_by() method --- simple_query_builder/querybuilder.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 53af9f4..ff1f939 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -336,18 +336,33 @@ def offset(self, offset: int = 0): self._sql += f" OFFSET {offset}" return self - def order_by(self, field: str = '', sort: str = 'ASC'): - if field == '' or sort == '': - self.set_error(f"Empty field or sort in {inspect.stack()[0][3]} method") - return self + def _prepare_sorting(self, field: str = '', sort: str = ''): + if field.find(' ') > -1: + splitted = field.split(' ') + field = splitted[0] + sort = splitted[1] - sort = sort.upper() field = field.replace('.', '`.`') + if sort == '': + sort = 'ASC' + else: + sort = sort.upper() + + return field, sort + + def order_by(self, field: str = '', sort: str = ''): + if field == '': + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") + return self + + field, sort = self._prepare_sorting(field, sort) + if sort in self._SORT_TYPES: self._sql += f" ORDER BY `{field}` {sort}" else: self._sql += f" ORDER BY `{field}`" + return self def group_by(self, field: str = ''): From d46ad639d57dd884e674980e85b09a658dd66185 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 11:40:45 +0500 Subject: [PATCH 13/83] refactor order_by() method --- simple_query_builder/querybuilder.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index ff1f939..76ca94d 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -336,7 +336,7 @@ def offset(self, offset: int = 0): self._sql += f" OFFSET {offset}" return self - def _prepare_sorting(self, field: str = '', sort: str = ''): + def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: if field.find(' ') > -1: splitted = field.split(' ') field = splitted[0] @@ -349,19 +349,23 @@ def _prepare_sorting(self, field: str = '', sort: str = ''): else: sort = sort.upper() - return field, sort + return f"`{field}`", sort - def order_by(self, field: str = '', sort: str = ''): - if field == '': + def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): + if field == '' or field == () or field == []: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self - field, sort = self._prepare_sorting(field, sort) + if isinstance(field, str): + field, sort = self._prepare_sorting(field, sort) - if sort in self._SORT_TYPES: - self._sql += f" ORDER BY `{field}` {sort}" - else: - self._sql += f" ORDER BY `{field}`" + if sort in self._SORT_TYPES: + self._sql += f" ORDER BY {field} {sort}" + else: + self._sql += f" ORDER BY {field}" + elif isinstance(field, tuple) or isinstance(field, list): + new_list = [f"{self._prepare_sorting(item)[0]} {self._prepare_sorting(item)[1]}" for item in field] + self._sql += ' ORDER BY ' + ', '.join(new_list) return self From 7f1cdee94fbf059653d7258cd9e4b909c218228c Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 11:57:15 +0500 Subject: [PATCH 14/83] add _prepare_fieldlist() method --- simple_query_builder/querybuilder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 76ca94d..c54df24 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -217,7 +217,7 @@ def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) self.set_error(f"Incorrect type of items in {inspect.stack()[0][3]} method") return '' - return ', '.join(sql) if not as_list else sql + return self._prepare_fieldlist(sql) if not as_list else sql def _prepare_conditions(self, where: Union[str, list]) -> dict: result = {'sql': '', 'values': []} @@ -351,6 +351,12 @@ def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: return f"`{field}`", sort + def _prepare_fieldlist(self, fields: Union[tuple, list] = ()) -> str: + if fields == () or fields == []: + self.set_error(f"Empty fieldlist in {inspect.stack()[0][3]} method") + return '' + return ', '.join(fields) + def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): if field == '' or field == () or field == []: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") @@ -365,7 +371,7 @@ def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): self._sql += f" ORDER BY {field}" elif isinstance(field, tuple) or isinstance(field, list): new_list = [f"{self._prepare_sorting(item)[0]} {self._prepare_sorting(item)[1]}" for item in field] - self._sql += ' ORDER BY ' + ', '.join(new_list) + self._sql += ' ORDER BY ' + self._prepare_fieldlist(new_list) return self From fd2e63a4e1be48c6612b02a15efc3b6c2961e9e0 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 14:21:43 +0500 Subject: [PATCH 15/83] add _prepare_field() method, refactor _prepare_fieldlist() method --- simple_query_builder/querybuilder.py | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index c54df24..ab7f490 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -342,7 +342,7 @@ def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: field = splitted[0] sort = splitted[1] - field = field.replace('.', '`.`') + field = self._prepare_field(field) if sort == '': sort = 'ASC' @@ -351,11 +351,31 @@ def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: return f"`{field}`", sort - def _prepare_fieldlist(self, fields: Union[tuple, list] = ()) -> str: - if fields == () or fields == []: - self.set_error(f"Empty fieldlist in {inspect.stack()[0][3]} method") + def _prepare_field(self, field: str = '') -> str: + if field == '': + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return '' - return ', '.join(fields) + + if field.find('(') > -1 or field.find(')') > -1: + return f"{field}" + else: + field = field.replace('.', '`.`') + field = field.replace(' AS ', '` AS `') + return f"`{field}`" + + def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: + result = '' + if fields == '' or fields == () or fields == []: + self.set_error(f"Empty fields in {inspect.stack()[0][3]} method") + return result + + if isinstance(fields, str): + result = self._prepare_field(fields) + elif isinstance(fields, tuple) or isinstance(fields, list): + fields = [f"{self._prepare_field(field)}" for field in fields] + result = ', '.join(fields) + + return result def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): if field == '' or field == () or field == []: From 0dd5345f537ada2340ac84332cbffcddb482ef57 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 14:23:15 +0500 Subject: [PATCH 16/83] refactor group_by() method --- simple_query_builder/querybuilder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index ab7f490..5ff112b 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -395,13 +395,14 @@ def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): return self - def group_by(self, field: str = ''): - if field == '': + def group_by(self, field: Union[str, tuple, list] = ()): + if field == '' or field == () or field == []: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self - field = field.replace('.', '`.`') - self._sql += f" GROUP BY `{field}`" + field = self._prepare_fieldlist(field) + self._sql += f" GROUP BY {field}" + return self def delete(self, table: Union[str, dict]): From 35a323a6f99169adf239b0694e470d15e9d16fbf Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 16:20:24 +0500 Subject: [PATCH 17/83] refactor _prepare_aliases() and _prepare_conditions() methods --- simple_query_builder/querybuilder.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 5ff112b..6e08e65 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -198,21 +198,14 @@ def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) for item in items: if isinstance(items, list): if isinstance(item, str): - new_item = item.replace('.', '`.`') - sql.append(f"`{new_item}`") + sql.append(f"{item}") elif isinstance(item, dict): - first_item = list(item.values())[0].replace('.', '`.`') + first_item = list(item.values())[0] alias = list(item.keys())[0] - if first_item.find('(') > -1 or first_item.find(')') > -1: - sql.append(f"{first_item}" if isinstance(alias, int) else f"{first_item} AS `{alias}`") - else: - sql.append(f"`{first_item}`" if isinstance(alias, int) else f"`{first_item}` AS `{alias}`") + sql.append(f"{first_item}" if isinstance(alias, int) else f"{first_item} AS {alias}") elif isinstance(items, dict): - new_item = items[item].replace('.', '`.`') - if new_item.find('(') > -1 or new_item.find(')') > -1: - sql.append(f"{new_item}" if isinstance(item, int) else f"{new_item} AS `{item}`") - else: - sql.append(f"`{new_item}`" if isinstance(item, int) else f"`{new_item}` AS `{item}`") + new_item = items[item] + sql.append(f"{new_item}" if isinstance(item, int) else f"{new_item} AS {item}") else: self.set_error(f"Incorrect type of items in {inspect.stack()[0][3]} method") return '' @@ -232,17 +225,17 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: for cond in where: if isinstance(cond, list): if len(cond) == 3: - field = cond[0].replace('.', '`.`') + field = self._prepare_field(cond[0]) operator = cond[1].upper() value = cond[2] if operator in self._OPERATORS: if operator == 'IN' and (isinstance(value, list) or isinstance(value, tuple)): values = ("?," * len(value)).rstrip(',') - sql += f"(`{field}` {operator} ({values}))" + sql += f"({field} {operator} ({values}))" for item in value: result['values'].append(item) else: - sql += f"({field} {operator} ?)" if field.find('(') > -1 or field.find(')') > -1 else f"(`{field}` {operator} ?)" + sql += f"({field} {operator} ?)" result['values'].append(value) elif isinstance(cond, str): upper = cond.upper() From d97084fde464fd4ec02532e946c3ee4210c4b0ae Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 6 Oct 2022 16:45:59 +0500 Subject: [PATCH 18/83] refactor insert() method --- simple_query_builder/querybuilder.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 6e08e65..d300167 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -365,7 +365,7 @@ def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: if isinstance(fields, str): result = self._prepare_field(fields) elif isinstance(fields, tuple) or isinstance(fields, list): - fields = [f"{self._prepare_field(field)}" for field in fields] + fields = [self._prepare_field(field) for field in fields] result = ', '.join(fields) return result @@ -393,8 +393,7 @@ def group_by(self, field: Union[str, tuple, list] = ()): self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self - field = self._prepare_fieldlist(field) - self._sql += f" GROUP BY {field}" + self._sql += f" GROUP BY {self._prepare_fieldlist(field)}" return self @@ -421,10 +420,8 @@ def insert(self, table: Union[str, dict], fields: Union[list, dict]): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict): - table = f"`{self._prepare_aliases(table)}`" - elif isinstance(table, str): - table = f"`{table}`" + if isinstance(table, dict) or isinstance(table, str): + table = self._prepare_aliases(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self @@ -433,14 +430,14 @@ def insert(self, table: Union[str, dict], fields: Union[list, dict]): if isinstance(fields, dict): values = ("?," * len(fields)).rstrip(',') - self._sql = f"INSERT INTO {table} (`" + '`, `'.join(list(fields.keys())) + f"`) VALUES ({values})" + self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(list(fields.keys())) + f") VALUES ({values})" self._params = tuple(fields.values()) elif isinstance(fields, list): names = fields.pop(0) value = ("?," * len(names)).rstrip(',') v = f"({value})," values = (v * len(fields)).rstrip(',') - self._sql = f"INSERT INTO {table} (`" + '`, `'.join(names) + f"`) VALUES {values}" + self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(names) + f") VALUES {values}" params = [] for item in fields: if isinstance(item, list): From 4623f29be0c080926ffa801bbf335725194a65dc Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 01:06:45 +0500 Subject: [PATCH 19/83] fix _prepare_field() method --- simple_query_builder/querybuilder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index d300167..741023e 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -350,7 +350,11 @@ def _prepare_field(self, field: str = '') -> str: return '' if field.find('(') > -1 or field.find(')') > -1: - return f"{field}" + if field.find(' AS ') > -1: + field = field.replace(' AS ', ' AS `') + return f"{field}`" + else: + return f"{field}" else: field = field.replace('.', '`.`') field = field.replace(' AS ', '` AS `') From 6a56cfca268ccc32b1fd4b2d125533e61c4afb56 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 01:14:11 +0500 Subject: [PATCH 20/83] refactor delete() method --- simple_query_builder/querybuilder.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 741023e..2bff0ea 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -406,10 +406,8 @@ def delete(self, table: Union[str, dict]): self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict): - table = f"`{self._prepare_aliases(table)}`" - elif isinstance(table, str): - table = f"`{table}`" + if isinstance(table, dict) or isinstance(table, str): + table = self._prepare_aliases(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self From d8e88b5543cf3cc3372d184baba1ffa8f26d0793 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 01:19:40 +0500 Subject: [PATCH 21/83] refactor update() method --- simple_query_builder/querybuilder.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 2bff0ea..596ed75 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -457,10 +457,8 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict): - table = f"`{self._prepare_aliases(table)}`" - elif isinstance(table, str): - table = f"`{table}`" + if isinstance(table, dict) or isinstance(table, str): + table = self._prepare_aliases(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self @@ -468,7 +466,7 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): if isinstance(fields, list) or isinstance(fields, dict): sets = '' for item in fields: - sets += f" `{item.replace('.', '`.`')}` = ?," + sets += f" {self._prepare_field(item)} = ?," sets = sets.rstrip(',') else: self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") From 779af60b737ad02414d1a671281078fc692321fc Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 10:10:54 +0500 Subject: [PATCH 22/83] refactor select() method --- simple_query_builder/querybuilder.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 596ed75..6c346cf 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -256,18 +256,14 @@ def select(self, table: Union[str, dict], fields: Union[str, list, dict] = '*'): self.reset() - if isinstance(fields, dict) or isinstance(fields, list): + if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): self._sql = f"SELECT {self._prepare_aliases(fields)}" - elif isinstance(fields, str): - self._sql = f"SELECT {fields}" else: self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self - if isinstance(table, dict): + if isinstance(table, dict) or isinstance(table, str): self._sql += f" FROM {self._prepare_aliases(table)}" - elif isinstance(table, str): - self._sql += f" FROM `{table}`" else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self From d7fd1e1adac18d196be26232ecafa39a29bb2833 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 10:11:18 +0500 Subject: [PATCH 23/83] refactor join() method --- simple_query_builder/querybuilder.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 6c346cf..e83bdea 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -485,18 +485,16 @@ def join(self, table: Union[str, dict] = '', on: Union[str, tuple, list] = (), j self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict): + if isinstance(table, dict) or isinstance(table, str): self._sql += f" {join_type} JOIN {self._prepare_aliases(table)}" - elif isinstance(table, str): - self._sql += f" {join_type} JOIN `{table}`" else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self if on: if isinstance(on, tuple) or isinstance(on, list): - field1 = f"`{on[0].replace('.', '`.`')}`" - field2 = f"`{on[1].replace('.', '`.`')}`" + field1 = self._prepare_field(on[0]) + field2 = self._prepare_field(on[1]) self._sql += f" ON {field1} = {field2}" elif isinstance(on, str): self._sql += f" ON {on}" From e89e0538724cc6eb3285e079ff026d69042b1e6f Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 10:31:42 +0500 Subject: [PATCH 24/83] refactor _prepare_conditions() method --- simple_query_builder/querybuilder.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index e83bdea..1284c9a 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -224,7 +224,20 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: elif isinstance(where, list): for cond in where: if isinstance(cond, list): - if len(cond) == 3: + if len(cond) == 2: + field = self._prepare_field(cond[0]) + value = cond[1] + if isinstance(value, list) or isinstance(value, tuple): + operator = 'IN' + values = ("?," * len(value)).rstrip(',') + sql += f"({field} {operator} ({values}))" + for item in value: + result['values'].append(item) + else: + operator = '=' + sql += f"({field} {operator} ?)" + result['values'].append(value) + elif len(cond) == 3: field = self._prepare_field(cond[0]) operator = cond[1].upper() value = cond[2] From 26affed17b7ea941f5d166de95f99afd337ebcfb Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 10:55:14 +0500 Subject: [PATCH 25/83] update version --- setup.cfg | 2 +- setup.py | 2 +- simple_query_builder/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 503a1b2..07304bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.3 +tag_build = 0.3.4 tag_date = 0 diff --git a/setup.py b/setup.py index 968d345..aea9aed 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ :copyright: (c) 2022 co0lc0der """ -version = '0.3.3' +version = '0.3.4' with open('README.md', encoding='utf-8') as f: long_description = f.read() diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index aa6e236..5260cf9 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -7,5 +7,5 @@ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.3' +__version__ = '0.3.4' __email__ = 'c0der@ya.ru' From 21c969e3842a0b2e3f6cf15a4dc7cc1e921f3143 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 10:58:41 +0500 Subject: [PATCH 26/83] fix _prepare_field() method --- simple_query_builder/querybuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 1284c9a..b203673 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -367,7 +367,7 @@ def _prepare_field(self, field: str = '') -> str: else: field = field.replace('.', '`.`') field = field.replace(' AS ', '` AS `') - return f"`{field}`" + return field if field == '*' else f"`{field}`" def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: result = '' From 4689ac9869518c39dcc56df9991e4ef129c61e94 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 11:16:19 +0500 Subject: [PATCH 27/83] fix _prepare_sorting() method --- simple_query_builder/querybuilder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index b203673..6394210 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -346,12 +346,9 @@ def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: field = self._prepare_field(field) - if sort == '': - sort = 'ASC' - else: - sort = sort.upper() + sort = 'ASC' if sort == '' else sort.upper() - return f"`{field}`", sort + return field, sort def _prepare_field(self, field: str = '') -> str: if field == '': From cc826f0a5d8f2cdacccc18bf2bbde2e3597038c6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 12:00:20 +0500 Subject: [PATCH 28/83] fix _prepare_field() and order_by() methods --- simple_query_builder/querybuilder.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 6394210..cb0f9a8 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -355,7 +355,7 @@ def _prepare_field(self, field: str = '') -> str: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return '' - if field.find('(') > -1 or field.find(')') > -1: + if field.find('(') > -1 or field.find(')') > -1 or field.find('*') > -1: if field.find(' AS ') > -1: field = field.replace(' AS ', ' AS `') return f"{field}`" @@ -364,7 +364,7 @@ def _prepare_field(self, field: str = '') -> str: else: field = field.replace('.', '`.`') field = field.replace(' AS ', '` AS `') - return field if field == '*' else f"`{field}`" + return f"`{field}`" def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: result = '' @@ -393,8 +393,11 @@ def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): else: self._sql += f" ORDER BY {field}" elif isinstance(field, tuple) or isinstance(field, list): - new_list = [f"{self._prepare_sorting(item)[0]} {self._prepare_sorting(item)[1]}" for item in field] - self._sql += ' ORDER BY ' + self._prepare_fieldlist(new_list) + new_list = [] + for item in field: + new_item = self._prepare_sorting(item) + new_list.append(f"{new_item[0]} {new_item[1]}") + self._sql += ' ORDER BY ' + ', '.join(new_list) return self From b2540fc447477d31926e36e4c34181a7bfb51dab Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 13:41:35 +0500 Subject: [PATCH 29/83] small types fixes --- simple_query_builder/querybuilder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index cb0f9a8..3b7a79a 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -138,19 +138,20 @@ def get_result(self) -> Union[tuple, list]: def get_count(self) -> int: return self._count - def reset(self) -> None: + def reset(self) -> bool: self._sql = '' self._params = () self._query = None self._result = [] self._count = -1 self.set_error() + return True - def all(self) -> Union[list, dict]: + def all(self) -> Union[tuple, list, dict, None]: self.query() return self._result - def one(self) -> Union[list, dict]: + def one(self) -> Union[tuple, list, dict, None]: self.query(self._sql, self._params, self._FETCH_ONE) return self._result @@ -158,7 +159,7 @@ def go(self) -> Union[int, None]: self.query(self._sql, self._params, self._NO_FETCH) return self._cur.lastrowid - def column(self, column=0): + def column(self, column: int = 0) -> Union[tuple, list, dict, None]: self.query('', (), self._FETCH_COLUMN, column) return self._result From dc7cfd36c8d1919783e85c8509ea2aa41a0ffbdf Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 13:52:22 +0500 Subject: [PATCH 30/83] add pluck() method --- simple_query_builder/querybuilder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 3b7a79a..62d18ab 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -163,6 +163,10 @@ def column(self, column: int = 0) -> Union[tuple, list, dict, None]: self.query('', (), self._FETCH_COLUMN, column) return self._result + def pluck(self, key: int = 0, column: int = 1) -> Union[tuple, list, dict, None]: + self.query() + return [(x[key], x[column]) for x in self._result] + def count(self, table: Union[str, dict], field: str = ''): if table == '' or table == {}: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") From 3dda0b1f11c22babea2f0fa108bfabc812a8ea73 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 7 Oct 2022 13:57:55 +0500 Subject: [PATCH 31/83] update README --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 81e7858..1c37659 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai - `reset()` resets state to default values (except PDO property) - `all()` executes SQL query and returns all rows of result (`fetchall()`) - `one()` executes SQL query and returns the first row of result (`fetchone()`) -- `column(col_index)` executes SQL query and returns the first column of result, `col_index` is `0` by default +- `column(col_index)` executes SQL query and returns the needed column of result, `col_index` is `0` by default +- `pluck(key_index, col_index)` executes SQL query and returns a list of tuples (the key (usually ID) and the needed column of result), `key_index` is `0` and `col_index` is `1` by default - `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) - `count()` prepares a query with SQL `COUNT(*)` function and executes it - `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries @@ -66,6 +67,8 @@ SELECT * FROM `users`; - Select a row with a condition ```python results = qb.select('users').where([['id', '=', 10]]).one() +# or since 0.3.4 +results = qb.select('users').where([['id', 10]]).one() ``` ```sql SELECT * FROM `users` WHERE `id` = 10; @@ -73,6 +76,8 @@ SELECT * FROM `users` WHERE `id` = 10; - Select rows with two conditions ```python results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', '=', 2]]).all() +# or since 0.3.4 +results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', 2]]).all() ``` ```sql SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); @@ -101,6 +106,12 @@ results = qb.select('posts')\ .offset(14)\ .limit(7)\ .all() +# or since 0.3.4 +results = qb.select('posts')\ + .where([['user_id', 3]])\ + .offset(14)\ + .limit(7)\ + .all() ``` ```sql SELECT * FROM `posts` WHERE (`user_id` = 3) OFFSET 14 LIMIT 7; @@ -118,8 +129,13 @@ SELECT COUNT(*) AS `counter` FROM `users`; 2. `ORDER BY` ```python results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ - .where([['b.id', '>', 1], 'and', ['b.parent_id', '=', 1]])\ - .orderBy('b.id', 'desc')\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ + .order_by('b.id', 'desc')\ + .all() +# or since 0.3.4 +results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ + .order_by('b.id desc')\ .all() ``` ```sql @@ -131,7 +147,7 @@ ORDER BY `b`.`id` DESC; ```python results = qb.select('posts', ['id', 'category', 'title'])\ .where([['views', '>=', 1000]])\ - .groupBy('category')\ + .group_by('category')\ .all() ``` ```sql @@ -141,14 +157,20 @@ WHERE (`views` >= 1000) GROUP BY `category`; ```python groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ .where([['YEAR(`created_at`)', '=', 2020]])\ - .groupBy('month_num')\ - .having([['total', '>', 20000]])\ + .group_by('month_num')\ + .having([['total', '=', 20000]])\ + .all() +# or since 0.3.4 +groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ + .where([['YEAR(`created_at`)', 2020]])\ + .group_by('month_num')\ + .having([['total', 20000]])\ .all() ``` ```sql SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` FROM `orders` WHERE (YEAR(`created_at`) = 2020) -GROUP BY `month_num` HAVING (`total` > 20000); +GROUP BY `month_num` HAVING (`total` = 20000); ``` 4. `JOIN`. Supports `INNER`, `LEFT OUTER`, `RIGHT OUTER`, `FULL OUTER` and `CROSS` joins (`INNER` is by default) ```python @@ -191,7 +213,36 @@ FROM `cabs_printers` AS `cp` INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` INNER JOIN `cartridge_types` AS `c` ON p.cartridge_id=c.id -WHERE (`cp`.`cab_id` IN (11,12,13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`); +WHERE (`cp`.`cab_id` IN (11, 12, 13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`); +``` +```python +# since 0.3.4 +results = qb.select({'cp': 'cabs_printers'}, [ + 'cp.id', + 'cp.cab_id', + {'cab_name': 'cb.name'}, + 'cp.printer_id', + {'cartridge_id': 'c.id'}, + {'printer_name': 'p.name'}, + {'cartridge_type': 'c.name'}, + 'cp.comment' + ])\ + .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ + .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ + .join({'c': 'cartridge_types'}, ['p.cartridge_id', 'c.id'])\ + .group_by(['cp.printer_id', 'cartridge_id'])\ + .order_by(['cp.cab_id', 'cp.printer_id desc'])\ + .all() +``` +```sql +SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, `c`.`id` AS `cartridge_id`, + `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` +FROM `cabs_printers` AS `cp` +INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` +INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` +INNER JOIN `cartridge_types` AS `c` ON `p`.`cartridge_id` = `c`.`id` +GROUP BY `cp`.`printer_id`, `cartridge_id` +ORDER BY `cp`.`cab_id` ASC, `cp`.`printer_id` DESC; ``` - Insert a row ```python @@ -228,6 +279,14 @@ qb.update('users', { .where([['id', '=', 7]])\ .limit()\ .go() +# or since 0.3.4 +qb.update('users', { + 'username': 'John Doe', + 'status': 'new status' + })\ + .where([['id', 7]])\ + .limit()\ + .go() ``` ```sql UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' @@ -249,6 +308,11 @@ qb.delete('users')\ .where([['name', '=', 'John']])\ .limit()\ .go() +# or since 0.3.4 +qb.delete('users')\ + .where([['name', 'John']])\ + .limit()\ + .go() ``` ```sql DELETE FROM `users` WHERE `name` = 'John' LIMIT 1; @@ -258,6 +322,10 @@ DELETE FROM `users` WHERE `name` = 'John' LIMIT 1; qb.delete('comments')\ .where([['user_id', '=', 10]])\ .go() +# or since 0.3.4 +qb.delete('comments')\ + .where([['user_id', 10]])\ + .go() ``` ```sql DELETE FROM `comments` WHERE `user_id` = 10; From a786b6ec368ead7753a9d578bf23b59863534da3 Mon Sep 17 00:00:00 2001 From: Danil Date: Sun, 12 Mar 2023 15:11:10 +0300 Subject: [PATCH 32/83] Update querybuilder.py --- simple_query_builder/querybuilder.py | 293 +++++++++++++++++---------- 1 file changed, 185 insertions(+), 108 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 62d18ab..0c53983 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -4,10 +4,10 @@ :copyright: (c) 2022 co0lc0der """ +import inspect import sqlite3 -import traceback import sys -import inspect +import traceback from typing import Union @@ -21,12 +21,12 @@ def __call__(cls, *args, **kwargs): class DataBase(metaclass=MetaSingleton): - db_name = 'db.db' + db_name = "db.db" conn = None cursor = None - def connect(self, db_name=''): - if db_name != '': + def connect(self, db_name=""): + if db_name != "": self.db_name = db_name if self.conn is None: @@ -40,10 +40,21 @@ def c(self): class QueryBuilder: - _OPERATORS: list = ['=', '>', '<', '>=', '<=', '!=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN'] - _LOGICS: list = ['AND', 'OR', 'NOT'] - _SORT_TYPES: list = ['ASC', 'DESC'] - _JOIN_TYPES: list = ['INNER', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'CROSS'] + _OPERATORS: list = [ + "=", + ">", + "<", + ">=", + "<=", + "!=", + "LIKE", + "NOT LIKE", + "IN", + "NOT IN", + ] + _LOGICS: list = ["AND", "OR", "NOT"] + _SORT_TYPES: list = ["ASC", "DESC"] + _JOIN_TYPES: list = ["INNER", "LEFT OUTER", "RIGHT OUTER", "FULL OUTER", "CROSS"] _NO_FETCH: int = 0 _FETCH_ONE: int = 1 _FETCH_ALL: int = 2 @@ -51,20 +62,22 @@ class QueryBuilder: _conn = None _cur = None _query = None - _sql: str = '' + _sql: str = "" _error: bool = False - _error_message: str = '' + _error_message: str = "" _result: Union[tuple, list] = [] _count: int = -1 _params: tuple = () - def __init__(self, database: DataBase, db_name='') -> None: + def __init__(self, database: DataBase, db_name="") -> None: self._conn = database.connect(db_name) # self._conn.row_factory = sqlite3.Row - # self._conn.row_factory = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)]) - self._cur = database.c() + self._conn.row_factory = lambda c, r: dict( + [(col[0], r[idx]) for idx, col in enumerate(c.description)] + ) + self._cur = self._conn.cursor() - def query(self, sql: str = '', params=(), fetch=2, column=0): + def query(self, sql: str = "", params=(), fetch=2, column=0): if fetch == 2: fetch = self._FETCH_ALL @@ -97,27 +110,35 @@ def query(self, sql: str = '', params=(), fetch=2, column=0): self.set_error() except sqlite3.Error as er: self._error = True - print('SQLite error: %s' % (' '.join(er.args))) + print("SQL: %s" % self._sql) + print("Params: %s" % str(self._params)) + print("SQLite error: %s" % (" ".join(er.args))) print("Exception class is: ", er.__class__) - print('SQLite traceback: ') + print("SQLite traceback: ") exc_type, exc_value, exc_tb = sys.exc_info() print(traceback.format_exception(exc_type, exc_value, exc_tb)) return self - def add_semicolon(self, sql: str = '') -> str: - new_sql = self._sql if sql == '' else sql + def add_semicolon(self, sql: str = "") -> str: + new_sql = self._sql if sql == "" else sql - if new_sql != '': - new_sql += ';' if new_sql[-1] != ';' else '' + if new_sql != "": + new_sql += ";" if new_sql[-1] != ";" else "" - if sql == '': + if sql == "": self._sql = new_sql return new_sql def get_sql(self) -> str: - return self._sql + # Replace ? with markers + sql = self._sql + params = self._params + if params: + for p in params: + sql = sql.replace("?", str(p), 1) + return sql def get_error(self) -> bool: return self._error @@ -125,8 +146,8 @@ def get_error(self) -> bool: def get_error_message(self) -> str: return self._error_message - def set_error(self, message: str = '') -> None: - self._error = message != '' + def set_error(self, message: str = "") -> None: + self._error = message != "" self._error_message = message def get_params(self) -> tuple: @@ -139,7 +160,7 @@ def get_count(self) -> int: return self._count def reset(self) -> bool: - self._sql = '' + self._sql = "" self._params = () self._query = None self._result = [] @@ -160,22 +181,22 @@ def go(self) -> Union[int, None]: return self._cur.lastrowid def column(self, column: int = 0) -> Union[tuple, list, dict, None]: - self.query('', (), self._FETCH_COLUMN, column) + self.query("", (), self._FETCH_COLUMN, column) return self._result def pluck(self, key: int = 0, column: int = 1) -> Union[tuple, list, dict, None]: self.query() return [(x[key], x[column]) for x in self._result] - def count(self, table: Union[str, dict], field: str = ''): - if table == '' or table == {}: + def count(self, table: Union[str, dict], field: str = ""): + if table == "" or table == {}: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if field == '': + if field == "": self.select(table, "COUNT(*) AS `counter`") else: - field = field.replace('.', '`.`') + field = field.replace(".", "`.`") self.select(table, f"COUNT(`{field}`) AS `counter`") return self.one()[0] @@ -191,10 +212,12 @@ def exists(self) -> bool: result = self.one() return self._count > 0 - def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) -> Union[str, list]: - if items == '' or items == {} or items == []: + def _prepare_aliases( + self, items: Union[str, list, dict], as_list: bool = False + ) -> Union[str, list]: + if items == "" or items == {} or items == []: self.set_error(f"Empty items in {inspect.stack()[0][3]} method") - return '' + return "" sql = [] if isinstance(items, str): @@ -207,19 +230,27 @@ def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) elif isinstance(item, dict): first_item = list(item.values())[0] alias = list(item.keys())[0] - sql.append(f"{first_item}" if isinstance(alias, int) else f"{first_item} AS {alias}") + sql.append( + f"{first_item}" + if isinstance(alias, int) + else f"{first_item} AS {alias}" + ) elif isinstance(items, dict): new_item = items[item] - sql.append(f"{new_item}" if isinstance(item, int) else f"{new_item} AS {item}") + sql.append( + f"{new_item}" + if isinstance(item, int) + else f"{new_item} AS {item}" + ) else: self.set_error(f"Incorrect type of items in {inspect.stack()[0][3]} method") - return '' + return "" return self._prepare_fieldlist(sql) if not as_list else sql def _prepare_conditions(self, where: Union[str, list]) -> dict: - result = {'sql': '', 'values': []} - sql = '' + result = {"sql": "", "values": []} + sql = "" if not where: return result @@ -232,29 +263,38 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: if len(cond) == 2: field = self._prepare_field(cond[0]) value = cond[1] - if isinstance(value, list) or isinstance(value, tuple): - operator = 'IN' - values = ("?," * len(value)).rstrip(',') + + if value.lower() == "is null": + operator = "IS NULL" + sql += f"({field} {operator})" + elif value.lower() == "is not null": + operator = "IS NOT NULL" + sql += f"({field} {operator})" + elif isinstance(value, list) or isinstance(value, tuple): + operator = "IN" + values = ("?," * len(value)).rstrip(",") sql += f"({field} {operator} ({values}))" for item in value: - result['values'].append(item) + result["values"].append(item) else: - operator = '=' + operator = "=" sql += f"({field} {operator} ?)" - result['values'].append(value) + result["values"].append(value) elif len(cond) == 3: field = self._prepare_field(cond[0]) operator = cond[1].upper() value = cond[2] if operator in self._OPERATORS: - if operator == 'IN' and (isinstance(value, list) or isinstance(value, tuple)): - values = ("?," * len(value)).rstrip(',') + if operator == "IN" and ( + isinstance(value, list) or isinstance(value, tuple) + ): + values = ("?," * len(value)).rstrip(",") sql += f"({field} {operator} ({values}))" for item in value: - result['values'].append(item) + result["values"].append(item) else: sql += f"({field} {operator} ?)" - result['values'].append(value) + result["values"].append(value) elif isinstance(cond, str): upper = cond.upper() if upper in self._LOGICS: @@ -263,50 +303,58 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: self.set_error(f"Incorrect type of where in {inspect.stack()[0][3]} method") return result - result['sql'] = sql + result["sql"] = sql return result - def select(self, table: Union[str, dict], fields: Union[str, list, dict] = '*'): - if table == '' or table == {} or fields == '' or fields == [] or fields == {}: + def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): + if table == "" or table == {} or fields == "" or fields == [] or fields == {}: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self self.reset() - if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): + if ( + isinstance(fields, dict) + or isinstance(fields, list) + or isinstance(fields, str) + ): self._sql = f"SELECT {self._prepare_aliases(fields)}" else: - self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") + self.set_error( + f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" + ) return self if isinstance(table, dict) or isinstance(table, str): self._sql += f" FROM {self._prepare_aliases(table)}" else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + self.set_error( + f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" + ) return self return self - def where(self, where: Union[str, list], addition: str = ''): - if where == '' or where == []: + def where(self, where: Union[str, list], addition: str = ""): + if where == "" or where == []: self.set_error(f"Empty where in {inspect.stack()[0][3]} method") return self conditions = self._prepare_conditions(where) - if addition != '': + if addition != "": self._sql += f" WHERE {conditions['sql']} {addition}" else: self._sql += f" WHERE {conditions['sql']}" - if isinstance(conditions['values'], list) and conditions['values'] != []: - self._params += tuple(conditions['values']) + if isinstance(conditions["values"], list) and conditions["values"] != []: + self._params += tuple(conditions["values"]) return self def having(self, having: Union[str, list]): - if having == '' or having == []: + if having == "" or having == []: self.set_error(f"Empty having in {inspect.stack()[0][3]} method") return self @@ -314,8 +362,8 @@ def having(self, having: Union[str, list]): self._sql += f" HAVING {conditions['sql']}" - if isinstance(conditions['values'], list) and conditions['values'] != []: - self._params += tuple(conditions['values']) + if isinstance(conditions["values"], list) and conditions["values"] != []: + self._params += tuple(conditions["values"]) return self @@ -324,7 +372,7 @@ def like(self, cond: Union[str, tuple, list] = ()): if isinstance(cond, str): self.where(cond) elif isinstance(cond, tuple) or isinstance(cond, list): - self.where([[cond[0], 'LIKE', cond[1]]]) + self.where([[cond[0], "LIKE", cond[1]]]) return self def not_like(self, cond: Union[str, tuple, list] = ()): @@ -332,7 +380,7 @@ def not_like(self, cond: Union[str, tuple, list] = ()): if isinstance(cond, str): self.where(cond) elif isinstance(cond, tuple) or isinstance(cond, list): - self.where([[cond[0], 'NOT LIKE', cond[1]]]) + self.where([[cond[0], "NOT LIKE", cond[1]]]) return self def limit(self, limit: int = 1): @@ -343,37 +391,37 @@ def offset(self, offset: int = 0): self._sql += f" OFFSET {offset}" return self - def _prepare_sorting(self, field: str = '', sort: str = '') -> tuple: - if field.find(' ') > -1: - splitted = field.split(' ') + def _prepare_sorting(self, field: str = "", sort: str = "") -> tuple: + if field.find(" ") > -1: + splitted = field.split(" ") field = splitted[0] sort = splitted[1] field = self._prepare_field(field) - sort = 'ASC' if sort == '' else sort.upper() + sort = "ASC" if sort == "" else sort.upper() return field, sort - def _prepare_field(self, field: str = '') -> str: - if field == '': + def _prepare_field(self, field: str = "") -> str: + if field == "": self.set_error(f"Empty field in {inspect.stack()[0][3]} method") - return '' + return "" - if field.find('(') > -1 or field.find(')') > -1 or field.find('*') > -1: - if field.find(' AS ') > -1: - field = field.replace(' AS ', ' AS `') + if field.find("(") > -1 or field.find(")") > -1 or field.find("*") > -1: + if field.find(" AS ") > -1: + field = field.replace(" AS ", " AS `") return f"{field}`" else: return f"{field}" else: - field = field.replace('.', '`.`') - field = field.replace(' AS ', '` AS `') + field = field.replace(".", "`.`") + field = field.replace(" AS ", "` AS `") return f"`{field}`" def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: - result = '' - if fields == '' or fields == () or fields == []: + result = "" + if fields == "" or fields == () or fields == []: self.set_error(f"Empty fields in {inspect.stack()[0][3]} method") return result @@ -381,12 +429,12 @@ def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: result = self._prepare_field(fields) elif isinstance(fields, tuple) or isinstance(fields, list): fields = [self._prepare_field(field) for field in fields] - result = ', '.join(fields) + result = ", ".join(fields) return result - def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): - if field == '' or field == () or field == []: + def order_by(self, field: Union[str, tuple, list] = (), sort: str = ""): + if field == "" or field == () or field == []: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self @@ -402,12 +450,12 @@ def order_by(self, field: Union[str, tuple, list] = (), sort: str = ''): for item in field: new_item = self._prepare_sorting(item) new_list.append(f"{new_item[0]} {new_item[1]}") - self._sql += ' ORDER BY ' + ', '.join(new_list) + self._sql += " ORDER BY " + ", ".join(new_list) return self def group_by(self, field: Union[str, tuple, list] = ()): - if field == '' or field == () or field == []: + if field == "" or field == () or field == []: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self @@ -416,14 +464,16 @@ def group_by(self, field: Union[str, tuple, list] = ()): return self def delete(self, table: Union[str, dict]): - if table == '' or table == {}: + if table == "" or table == {}: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + self.set_error( + f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" + ) return self self.reset() @@ -432,28 +482,38 @@ def delete(self, table: Union[str, dict]): return self def insert(self, table: Union[str, dict], fields: Union[list, dict]): - if table == '' or table == {} or fields == [] or fields == {}: + if table == "" or table == {} or fields == [] or fields == {}: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + self.set_error( + f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" + ) return self self.reset() if isinstance(fields, dict): - values = ("?," * len(fields)).rstrip(',') - self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(list(fields.keys())) + f") VALUES ({values})" + values = ("?," * len(fields)).rstrip(",") + self._sql = ( + f"INSERT INTO {table} (" + + self._prepare_fieldlist(list(fields.keys())) + + f") VALUES ({values})" + ) self._params = tuple(fields.values()) elif isinstance(fields, list): names = fields.pop(0) - value = ("?," * len(names)).rstrip(',') + value = ("?," * len(names)).rstrip(",") v = f"({value})," - values = (v * len(fields)).rstrip(',') - self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(names) + f") VALUES {values}" + values = (v * len(fields)).rstrip(",") + self._sql = ( + f"INSERT INTO {table} (" + + self._prepare_fieldlist(names) + + f") VALUES {values}" + ) params = [] for item in fields: if isinstance(item, list): @@ -461,29 +521,35 @@ def insert(self, table: Union[str, dict], fields: Union[list, dict]): params.append(subitem) self._params = tuple(params) else: - self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") + self.set_error( + f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" + ) return self return self def update(self, table: Union[str, dict], fields: Union[list, dict]): - if table == '' or table == {} or fields == [] or fields == {}: + if table == "" or table == {} or fields == [] or fields == {}: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + self.set_error( + f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" + ) return self if isinstance(fields, list) or isinstance(fields, dict): - sets = '' + sets = "" for item in fields: sets += f" {self._prepare_field(item)} = ?," - sets = sets.rstrip(',') + sets = sets.rstrip(",") else: - self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") + self.set_error( + f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" + ) return self self.reset() @@ -493,20 +559,29 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): return self - def join(self, table: Union[str, dict] = '', on: Union[str, tuple, list] = (), join_type: str = 'INNER'): + def join( + self, + table: Union[str, dict] = "", + on: Union[str, tuple, list] = (), + join_type: str = "INNER", + ): join_type = join_type.upper() - if join_type == '' or join_type not in self._JOIN_TYPES: - self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method") + if join_type == "" or join_type not in self._JOIN_TYPES: + self.set_error( + f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method" + ) return self - if table == '' or table == {}: + if table == "" or table == {}: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): self._sql += f" {join_type} JOIN {self._prepare_aliases(table)}" else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + self.set_error( + f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" + ) return self if on: @@ -517,7 +592,9 @@ def join(self, table: Union[str, dict] = '', on: Union[str, tuple, list] = (), j elif isinstance(on, str): self._sql += f" ON {on}" else: - self.set_error(f"Incorrect type of on in {inspect.stack()[0][3]} method. On must be String, Tuple or List") + self.set_error( + f"Incorrect type of on in {inspect.stack()[0][3]} method. On must be String, Tuple or List" + ) return self self.set_error() @@ -525,11 +602,11 @@ def join(self, table: Union[str, dict] = '', on: Union[str, tuple, list] = (), j return self def drop(self, table: str, add_exists: bool = True): - if table == '': + if table == "": self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - exists = 'IF EXISTS ' if add_exists else '' + exists = "IF EXISTS " if add_exists else "" self.reset() self._sql = f"DROP TABLE {exists}`{table}`" @@ -537,7 +614,7 @@ def drop(self, table: str, add_exists: bool = True): return self def truncate(self, table: str): - if table == '': + if table == "": self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self From 61e03a1d488f95c991de70f31bb0d940c9991283 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 11:31:28 +0500 Subject: [PATCH 33/83] add print_error property & refactor set_error() method --- simple_query_builder/querybuilder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 0c53983..37edef8 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -65,12 +65,14 @@ class QueryBuilder: _sql: str = "" _error: bool = False _error_message: str = "" + _print_errors: bool = False _result: Union[tuple, list] = [] _count: int = -1 _params: tuple = () - def __init__(self, database: DataBase, db_name="") -> None: + def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, print_errors: bool = False) -> None: self._conn = database.connect(db_name) + self._print_errors = print_errors # self._conn.row_factory = sqlite3.Row self._conn.row_factory = lambda c, r: dict( [(col[0], r[idx]) for idx, col in enumerate(c.description)] @@ -144,11 +146,15 @@ def get_error(self) -> bool: return self._error def get_error_message(self) -> str: + if self._print_errors and self._error: + print(self._error_message) return self._error_message def set_error(self, message: str = "") -> None: - self._error = message != "" + self._error = bool(message) self._error_message = message + if self._print_errors and self._error: + print(self._error_message) def get_params(self) -> tuple: return self._params From 72183e8292b825d36c3025dbc0f25c8a4bdb8e15 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 11:32:53 +0500 Subject: [PATCH 34/83] add result_dict parameter into __init__() method --- simple_query_builder/querybuilder.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 37edef8..8fcca6b 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -74,9 +74,10 @@ def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = Tr self._conn = database.connect(db_name) self._print_errors = print_errors # self._conn.row_factory = sqlite3.Row - self._conn.row_factory = lambda c, r: dict( - [(col[0], r[idx]) for idx, col in enumerate(c.description)] - ) + if result_dict: + self._conn.row_factory = lambda c, r: dict( + [(col[0], r[idx]) for idx, col in enumerate(c.description)] + ) self._cur = self._conn.cursor() def query(self, sql: str = "", params=(), fetch=2, column=0): From 36f573320f3bee3c12b8fd188f5ce7f4822420eb Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 11:35:14 +0500 Subject: [PATCH 35/83] some refactor --- simple_query_builder/querybuilder.py | 150 +++++++++++---------------- 1 file changed, 62 insertions(+), 88 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 8fcca6b..e8f5ff2 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -52,9 +52,22 @@ class QueryBuilder: "IN", "NOT IN", ] - _LOGICS: list = ["AND", "OR", "NOT"] - _SORT_TYPES: list = ["ASC", "DESC"] - _JOIN_TYPES: list = ["INNER", "LEFT OUTER", "RIGHT OUTER", "FULL OUTER", "CROSS"] + _LOGICS: list = [ + "AND", + "OR", + "NOT" + ] + _SORT_TYPES: list = [ + "ASC", + "DESC" + ] + _JOIN_TYPES: list = [ + "INNER", + "LEFT OUTER", + "RIGHT OUTER", + "FULL OUTER", + "CROSS" + ] _NO_FETCH: int = 0 _FETCH_ONE: int = 1 _FETCH_ALL: int = 2 @@ -124,12 +137,12 @@ def query(self, sql: str = "", params=(), fetch=2, column=0): return self def add_semicolon(self, sql: str = "") -> str: - new_sql = self._sql if sql == "" else sql + new_sql = self._sql if not sql else sql - if new_sql != "": + if new_sql: new_sql += ";" if new_sql[-1] != ";" else "" - if sql == "": + if not sql: self._sql = new_sql return new_sql @@ -196,11 +209,11 @@ def pluck(self, key: int = 0, column: int = 1) -> Union[tuple, list, dict, None] return [(x[key], x[column]) for x in self._result] def count(self, table: Union[str, dict], field: str = ""): - if table == "" or table == {}: + if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if field == "": + if not field: self.select(table, "COUNT(*) AS `counter`") else: field = field.replace(".", "`.`") @@ -219,10 +232,8 @@ def exists(self) -> bool: result = self.one() return self._count > 0 - def _prepare_aliases( - self, items: Union[str, list, dict], as_list: bool = False - ) -> Union[str, list]: - if items == "" or items == {} or items == []: + def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) -> Union[str, list]: + if not items: self.set_error(f"Empty items in {inspect.stack()[0][3]} method") return "" @@ -233,22 +244,18 @@ def _prepare_aliases( for item in items: if isinstance(items, list): if isinstance(item, str): - sql.append(f"{item}") + sql.append(item) elif isinstance(item, dict): first_item = list(item.values())[0] alias = list(item.keys())[0] sql.append( - f"{first_item}" + first_item if isinstance(alias, int) else f"{first_item} AS {alias}" ) elif isinstance(items, dict): new_item = items[item] - sql.append( - f"{new_item}" - if isinstance(item, int) - else f"{new_item} AS {item}" - ) + sql.append(new_item if isinstance(item, int) else f"{new_item} AS {item}") else: self.set_error(f"Incorrect type of items in {inspect.stack()[0][3]} method") return "" @@ -256,14 +263,17 @@ def _prepare_aliases( return self._prepare_fieldlist(sql) if not as_list else sql def _prepare_conditions(self, where: Union[str, list]) -> dict: - result = {"sql": "", "values": []} + result = { + "sql": "", + "values": [] + } sql = "" if not where: return result if isinstance(where, str): - sql += f"{where}" + sql += where elif isinstance(where, list): for cond in where: if isinstance(cond, list): @@ -315,42 +325,34 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: return result def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): - if table == "" or table == {} or fields == "" or fields == [] or fields == {}: + if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self self.reset() - if ( - isinstance(fields, dict) - or isinstance(fields, list) - or isinstance(fields, str) - ): + if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): self._sql = f"SELECT {self._prepare_aliases(fields)}" else: - self.set_error( - f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" - ) + self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self if isinstance(table, dict) or isinstance(table, str): self._sql += f" FROM {self._prepare_aliases(table)}" else: - self.set_error( - f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" - ) + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self return self def where(self, where: Union[str, list], addition: str = ""): - if where == "" or where == []: + if not where: self.set_error(f"Empty where in {inspect.stack()[0][3]} method") return self conditions = self._prepare_conditions(where) - if addition != "": + if addition: self._sql += f" WHERE {conditions['sql']} {addition}" else: self._sql += f" WHERE {conditions['sql']}" @@ -361,7 +363,7 @@ def where(self, where: Union[str, list], addition: str = ""): return self def having(self, having: Union[str, list]): - if having == "" or having == []: + if not having: self.set_error(f"Empty having in {inspect.stack()[0][3]} method") return self @@ -411,7 +413,7 @@ def _prepare_sorting(self, field: str = "", sort: str = "") -> tuple: return field, sort def _prepare_field(self, field: str = "") -> str: - if field == "": + if not field: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return "" @@ -420,7 +422,7 @@ def _prepare_field(self, field: str = "") -> str: field = field.replace(" AS ", " AS `") return f"{field}`" else: - return f"{field}" + return field else: field = field.replace(".", "`.`") field = field.replace(" AS ", "` AS `") @@ -428,7 +430,7 @@ def _prepare_field(self, field: str = "") -> str: def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: result = "" - if fields == "" or fields == () or fields == []: + if not fields: self.set_error(f"Empty fields in {inspect.stack()[0][3]} method") return result @@ -441,7 +443,7 @@ def _prepare_fieldlist(self, fields: Union[str, tuple, list] = ()) -> str: return result def order_by(self, field: Union[str, tuple, list] = (), sort: str = ""): - if field == "" or field == () or field == []: + if not field: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self @@ -462,7 +464,7 @@ def order_by(self, field: Union[str, tuple, list] = (), sort: str = ""): return self def group_by(self, field: Union[str, tuple, list] = ()): - if field == "" or field == () or field == []: + if not field: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return self @@ -471,16 +473,14 @@ def group_by(self, field: Union[str, tuple, list] = ()): return self def delete(self, table: Union[str, dict]): - if table == "" or table == {}: + if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error( - f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" - ) + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self self.reset() @@ -489,38 +489,28 @@ def delete(self, table: Union[str, dict]): return self def insert(self, table: Union[str, dict], fields: Union[list, dict]): - if table == "" or table == {} or fields == [] or fields == {}: + if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error( - f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" - ) + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self self.reset() if isinstance(fields, dict): values = ("?," * len(fields)).rstrip(",") - self._sql = ( - f"INSERT INTO {table} (" - + self._prepare_fieldlist(list(fields.keys())) - + f") VALUES ({values})" - ) + self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(list(fields.keys())) + f") VALUES ({values})" self._params = tuple(fields.values()) elif isinstance(fields, list): names = fields.pop(0) value = ("?," * len(names)).rstrip(",") v = f"({value})," values = (v * len(fields)).rstrip(",") - self._sql = ( - f"INSERT INTO {table} (" - + self._prepare_fieldlist(names) - + f") VALUES {values}" - ) + self._sql = f"INSERT INTO {table} (" + self._prepare_fieldlist(names) + f") VALUES {values}" params = [] for item in fields: if isinstance(item, list): @@ -528,24 +518,20 @@ def insert(self, table: Union[str, dict], fields: Union[list, dict]): params.append(subitem) self._params = tuple(params) else: - self.set_error( - f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" - ) + self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self return self def update(self, table: Union[str, dict], fields: Union[list, dict]): - if table == "" or table == {} or fields == [] or fields == {}: + if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: - self.set_error( - f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" - ) + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self if isinstance(fields, list) or isinstance(fields, dict): @@ -554,9 +540,7 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): sets += f" {self._prepare_field(item)} = ?," sets = sets.rstrip(",") else: - self.set_error( - f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary" - ) + self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self self.reset() @@ -566,29 +550,20 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): return self - def join( - self, - table: Union[str, dict] = "", - on: Union[str, tuple, list] = (), - join_type: str = "INNER", - ): + def join(self, table: Union[str, dict] = "", on: Union[str, tuple, list] = (), join_type: str = "INNER"): join_type = join_type.upper() if join_type == "" or join_type not in self._JOIN_TYPES: - self.set_error( - f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method" - ) + self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method") return self - if table == "" or table == {}: + if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self if isinstance(table, dict) or isinstance(table, str): self._sql += f" {join_type} JOIN {self._prepare_aliases(table)}" else: - self.set_error( - f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary" - ) + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self if on: @@ -599,17 +574,15 @@ def join( elif isinstance(on, str): self._sql += f" ON {on}" else: - self.set_error( - f"Incorrect type of on in {inspect.stack()[0][3]} method. On must be String, Tuple or List" - ) + self.set_error(f"Incorrect type of on in {inspect.stack()[0][3]} method. On must be String, Tuple or List") return self self.set_error() - return self def drop(self, table: str, add_exists: bool = True): - if table == "": + # this method will be moved to another class + if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self @@ -621,7 +594,8 @@ def drop(self, table: str, add_exists: bool = True): return self def truncate(self, table: str): - if table == "": + # this method will be moved to another class + if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self From 5657a32d1949e784080c653f6daddc8e0e50d9b2 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 11:40:12 +0500 Subject: [PATCH 36/83] add is_null() & is_not_null() methods, not_null() as a synonym --- simple_query_builder/querybuilder.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index e8f5ff2..a5e22b1 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -392,6 +392,24 @@ def not_like(self, cond: Union[str, tuple, list] = ()): self.where([[cond[0], "NOT LIKE", cond[1]]]) return self + def is_null(self, field: str = ""): + if not field: + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") + return self + self.where([[field, "IS NULL"]]) + return self + + def is_not_null(self, field: str): + if not field: + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") + return self + self.where([[field, "IS NOT NULL"]]) + return self + + def not_null(self, field: str): + self.is_not_null(field) + return self + def limit(self, limit: int = 1): self._sql += f" LIMIT {limit}" return self From 43768f53b4e214aaa9f3a59a424971142bddf81e Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 12:02:39 +0500 Subject: [PATCH 37/83] refactor like() & not_like() methods --- simple_query_builder/querybuilder.py | 36 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index a5e22b1..aafa56a 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -376,20 +376,32 @@ def having(self, having: Union[str, list]): return self - def like(self, cond: Union[str, tuple, list] = ()): - if cond: - if isinstance(cond, str): - self.where(cond) - elif isinstance(cond, tuple) or isinstance(cond, list): - self.where([[cond[0], "LIKE", cond[1]]]) + def like(self, field: Union[str, tuple, list] = (), value: str = ""): + if not field: + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") + return self + + if isinstance(field, str) and field and isinstance(value, str) and value: + self.where([[field, "LIKE", value]]) + elif isinstance(field, str) and not value: + self.where(field) + elif isinstance(field, tuple) or isinstance(field, list): + self.where([[field[0], "LIKE", field[1]]]) + return self - def not_like(self, cond: Union[str, tuple, list] = ()): - if cond: - if isinstance(cond, str): - self.where(cond) - elif isinstance(cond, tuple) or isinstance(cond, list): - self.where([[cond[0], "NOT LIKE", cond[1]]]) + def not_like(self, field: Union[str, tuple, list] = (), value: str = ""): + if not field: + self.set_error(f"Empty field in {inspect.stack()[0][3]} method") + return self + + if isinstance(field, str) and isinstance(value, str) and value: + self.where([[field, "NOT LIKE", value]]) + elif isinstance(field, str) and not value: + self.where(field) + elif isinstance(field, tuple) or isinstance(field, list): + self.where([[field[0], "NOT LIKE", field[1]]]) + return self def is_null(self, field: str = ""): From 7a981070e3ee6086050c1e69a863ad43b218d547 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 12:03:50 +0500 Subject: [PATCH 38/83] update README --- README.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1c37659..c422f7e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # QueryBuilder python module -![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) -![PyPI - Downloads](https://img.shields.io/pypi/dm/simple-query-builder?color=darkgreen&style=flat-square) [![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) ![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) -![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) [![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) +![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) +![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) +![PyPI - Downloads](https://img.shields.io/pypi/dm/simple-query-builder?color=darkgreen&style=flat-square) This is a small easy-to-use module for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). @@ -46,6 +46,7 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai - `column(col_index)` executes SQL query and returns the needed column of result, `col_index` is `0` by default - `pluck(key_index, col_index)` executes SQL query and returns a list of tuples (the key (usually ID) and the needed column of result), `key_index` is `0` and `col_index` is `1` by default - `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) +- `exists()` returns `True` if SQL query has a row - `count()` prepares a query with SQL `COUNT(*)` function and executes it - `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries - 'SQL' methods are presented in [Usage section](#usage-examples) @@ -85,6 +86,8 @@ SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); - Select a row with a `LIKE` and `NOT LIKE` condition ```python results = qb.select('users').like(['name', '%John%']).all() +# or since 0.3.5 +results = qb.select('users').like('name', '%John%').all() # or results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() ``` @@ -92,13 +95,34 @@ results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() SELECT * FROM `users` WHERE (`name` LIKE '%John%'); ``` ```python -results = qb.select('users').notLike(['name', '%John%']).all() +results = qb.select('users').not_like(['name', '%John%']).all() +# or since 0.3.5 +results = qb.select('users').not_like('name', '%John%').all() # or results = qb.select('users').where([['name', 'NOT LIKE', '%John%']]).all() ``` ```sql SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%'); ``` +- Select a row with a `IS NULL` and `IS NOT NULL` condition (since 0.3.5) +```python +results = qb.select('users').is_null('phone').all() +# or +results = qb.select('users').where([['phone', 'is null']]).all() +``` +```sql +SELECT * FROM `users` WHERE (`phone` IS NULL); +``` +```python +results = qb.select('customers').is_not_null('address').all() +# or +results = qb.select('customers').not_null('address').all() +# or +results = qb.select('customers').where([['address', 'is not null']]).all() +``` +```sql +SELECT * FROM `customers` WHERE (`address` IS NOT NULL); +``` - Select rows with `OFFSET` and `LIMIT` ```python results = qb.select('posts')\ @@ -331,6 +355,8 @@ qb.delete('comments')\ DELETE FROM `comments` WHERE `user_id` = 10; ``` - Truncate a table + +This method will be moved to another class ```python qb.truncate('users').go() ``` @@ -338,6 +364,8 @@ qb.truncate('users').go() TRUNCATE TABLE `users`; ``` - Drop a table + +This method will be moved to another class ```python qb.drop('temporary').go() ``` From 6eb5cdb5c39fe1fe2edfecdf075332f5ad6143d2 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 13:33:41 +0500 Subject: [PATCH 39/83] refactor get_sql() method --- simple_query_builder/querybuilder.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index aafa56a..236d62f 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -148,12 +148,15 @@ def add_semicolon(self, sql: str = "") -> str: return new_sql def get_sql(self) -> str: - # Replace ? with markers sql = self._sql params = self._params if params: + # Replace ? with markers for p in params: - sql = sql.replace("?", str(p), 1) + if isinstance(p, str): + sql = sql.replace("?", f"'{p}'", 1) + else: + sql = sql.replace("?", str(p), 1) return sql def get_error(self) -> bool: From 1811d583391d83869915369cc1706b4cecfc2eae Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 14:11:06 +0500 Subject: [PATCH 40/83] update README --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c422f7e..4437443 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai ## How to use ### Main public methods - `get_sql()` returns SQL query string which will be executed -- `get_params()` returns an array of parameters for a query +- `get_params()` returns a tuple of parameters for a query - `get_result()` returns query's result - `get_count()` returns result's rows count - `get_error()` returns `True` if an error is had @@ -40,7 +40,7 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai - `set_error(message)` sets `_error` to `True` and `_error_essage` - `get_first()` returns the first item of results - `get_last()` returns the last item of results -- `reset()` resets state to default values (except PDO property) +- `reset()` resets state to default values - `all()` executes SQL query and returns all rows of result (`fetchall()`) - `one()` executes SQL query and returns the first row of result (`fetchone()`) - `column(col_index)` executes SQL query and returns the needed column of result, `col_index` is `0` by default @@ -55,7 +55,14 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai ```python from simple_query_builder import * -qb = QueryBuilder(DataBase(), 'my_db.db') +# if you want to get results as a list of dictionaries (by default since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db') # result_dict=True, print_errors=False + +# or if you want to get results as a list of tuples (since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db', result_dict=False) + +# for printing errors into terminal (since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db', print_errors=True) ``` ### Usage examples - Select all rows from a table @@ -86,20 +93,20 @@ SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); - Select a row with a `LIKE` and `NOT LIKE` condition ```python results = qb.select('users').like(['name', '%John%']).all() -# or since 0.3.5 -results = qb.select('users').like('name', '%John%').all() # or results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() +# or since 0.3.5 +results = qb.select('users').like('name', '%John%').all() ``` ```sql SELECT * FROM `users` WHERE (`name` LIKE '%John%'); ``` ```python results = qb.select('users').not_like(['name', '%John%']).all() -# or since 0.3.5 -results = qb.select('users').not_like('name', '%John%').all() # or results = qb.select('users').where([['name', 'NOT LIKE', '%John%']]).all() +# or since 0.3.5 +results = qb.select('users').not_like('name', '%John%').all() ``` ```sql SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%'); From 15f3431a63ee223c66d458c7ee16ebd1f7427fd1 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 14:14:16 +0500 Subject: [PATCH 41/83] update package version --- setup.cfg | 2 +- setup.py | 4 ++-- simple_query_builder/__init__.py | 4 ++-- simple_query_builder/querybuilder.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 07304bf..7c69ef9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.4 +tag_build = 0.3.5 tag_date = 0 diff --git a/setup.py b/setup.py index aea9aed..4cb6a45 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,10 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022 co0lc0der +:copyright: (c) 2022-2023 co0lc0der """ -version = '0.3.4' +version = '0.3.5' with open('README.md', encoding='utf-8') as f: long_description = f.read() diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index 5260cf9..09f2b32 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -1,11 +1,11 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022 co0lc0der +:copyright: (c) 2022-2023 co0lc0der """ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.4' +__version__ = '0.3.5' __email__ = 'c0der@ya.ru' diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 236d62f..fe9ec14 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -1,7 +1,7 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022 co0lc0der +:copyright: (c) 2022-2023 co0lc0der """ import inspect From 878fb8b433dc8b4cd2c6cc7ec515fee493e7db5d Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 17 Mar 2023 15:17:04 +0500 Subject: [PATCH 42/83] fix 2 typos in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4437443..0cddb03 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) ![PyPI - Downloads](https://img.shields.io/pypi/dm/simple-query-builder?color=darkgreen&style=flat-square) -This is a small easy-to-use module for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _list_ by default. At present time the component supports SQLite (file or memory). +This is a small easy-to-use module for working with a database. It provides some public methods to compose SQL queries and manipulate data. Each SQL query is prepared and safe. QueryBuilder fetches data to _dictionary_ by default. At present time the component supports SQLite (file or memory). ## Contributing @@ -37,7 +37,7 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai - `get_count()` returns result's rows count - `get_error()` returns `True` if an error is had - `get_error_message()` returns an error message if an error is had -- `set_error(message)` sets `_error` to `True` and `_error_essage` +- `set_error(message)` sets `_error` to `True` and `_error_message` - `get_first()` returns the first item of results - `get_last()` returns the last item of results - `reset()` resets state to default values From c31095bbf51f8afaa334df01cd1c43536c1ecf29 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 16:37:17 +0500 Subject: [PATCH 43/83] fix type error in _prepare_conditions() --- simple_query_builder/querybuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index fe9ec14..84f2de6 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -284,10 +284,10 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: field = self._prepare_field(cond[0]) value = cond[1] - if value.lower() == "is null": + if isinstance(value, str) and value.lower() == "is null": operator = "IS NULL" sql += f"({field} {operator})" - elif value.lower() == "is not null": + elif isinstance(value, str) and value.lower() == "is not null": operator = "IS NOT NULL" sql += f"({field} {operator})" elif isinstance(value, list) or isinstance(value, tuple): From 458f6394ecb0a7af0f0f51e4e0f1a45936959d8b Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 16:49:38 +0500 Subject: [PATCH 44/83] add _set_row_factory() method --- simple_query_builder/querybuilder.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 84f2de6..6664fcf 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -80,18 +80,23 @@ class QueryBuilder: _error_message: str = "" _print_errors: bool = False _result: Union[tuple, list] = [] + _result_dict = True _count: int = -1 _params: tuple = () def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, print_errors: bool = False) -> None: self._conn = database.connect(db_name) self._print_errors = print_errors + self._set_row_factory(result_dict) + self._cur = self._conn.cursor() + + def _set_row_factory(self, result_dict: bool = True): + self._result_dict = result_dict # self._conn.row_factory = sqlite3.Row - if result_dict: + if self._result_dict: self._conn.row_factory = lambda c, r: dict( [(col[0], r[idx]) for idx, col in enumerate(c.description)] ) - self._cur = self._conn.cursor() def query(self, sql: str = "", params=(), fetch=2, column=0): if fetch == 2: From 7d547f711400f1d9c0295f268d01bdbcdf90e702 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 16:50:14 +0500 Subject: [PATCH 45/83] fix pluck() method --- simple_query_builder/querybuilder.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 6664fcf..4f39003 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -212,7 +212,15 @@ def column(self, column: int = 0) -> Union[tuple, list, dict, None]: self.query("", (), self._FETCH_COLUMN, column) return self._result - def pluck(self, key: int = 0, column: int = 1) -> Union[tuple, list, dict, None]: + def pluck(self, key: Union[str, int] = 0, column: Union[str, int] = 1): + if ( + self._result_dict and (isinstance(key, int) or isinstance(column, int)) + ) or ( + not self._result_dict and (isinstance(key, str) or isinstance(column, str)) + ): + self.set_error(f"Incorrect type of key or column in {inspect.stack()[0][3]} method. Result dict is {self._result_dict}") + return self + self.query() return [(x[key], x[column]) for x in self._result] From 3b85f961572212dda01c48216424db52ca7c860a Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 17:02:53 +0500 Subject: [PATCH 46/83] refactor column() method --- simple_query_builder/querybuilder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 4f39003..2c88650 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -208,7 +208,11 @@ def go(self) -> Union[int, None]: self.query(self._sql, self._params, self._NO_FETCH) return self._cur.lastrowid - def column(self, column: int = 0) -> Union[tuple, list, dict, None]: + def column(self, column: Union[str, int] = 0): + if (self._result_dict and isinstance(column, int)) or (not self._result_dict and isinstance(column, str)): + self.set_error(f"Incorrect type of column in {inspect.stack()[0][3]} method. Result dict is {self._result_dict}") + return self + self.query("", (), self._FETCH_COLUMN, column) return self._result From 584d2657fe3fb046a52ed1878250061608bd6904 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 17:10:56 +0500 Subject: [PATCH 47/83] add type hints for query() method --- simple_query_builder/querybuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 2c88650..e90adf8 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -98,7 +98,7 @@ def _set_row_factory(self, result_dict: bool = True): [(col[0], r[idx]) for idx, col in enumerate(c.description)] ) - def query(self, sql: str = "", params=(), fetch=2, column=0): + def query(self, sql: str = "", params: tuple = (), fetch: int = 2, column: Union[str, int] = 0): if fetch == 2: fetch = self._FETCH_ALL From 26a310b7443fc48a2848e51f3bb8d9f08551bfd7 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 17:11:51 +0500 Subject: [PATCH 48/83] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0cddb03..5311f29 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ pip install https://github.com/co0lc0der/simple-query-builder-python/archive/mai - `reset()` resets state to default values - `all()` executes SQL query and returns all rows of result (`fetchall()`) - `one()` executes SQL query and returns the first row of result (`fetchone()`) -- `column(col_index)` executes SQL query and returns the needed column of result, `col_index` is `0` by default -- `pluck(key_index, col_index)` executes SQL query and returns a list of tuples (the key (usually ID) and the needed column of result), `key_index` is `0` and `col_index` is `1` by default +- `column(col)` executes SQL query and returns the needed column of result by its index or name, `col` is `0` by default +- `pluck(key, col)` executes SQL query and returns a list of tuples/dicts (the key (usually ID) and the needed column of result) by its indexes or names, `key` is `0` and `col` is `1` by default - `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) -- `exists()` returns `True` if SQL query has a row +- `exists()` returns `True` if SQL query has a row and `False` if it hasn't - `count()` prepares a query with SQL `COUNT(*)` function and executes it - `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries - 'SQL' methods are presented in [Usage section](#usage-examples) From 5c82e4801c8f1a9412102fa51c98de875ae146a6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Sat, 18 Mar 2023 17:12:27 +0500 Subject: [PATCH 49/83] update package version --- setup.cfg | 2 +- setup.py | 2 +- simple_query_builder/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7c69ef9..a7468af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.5 +tag_build = 0.3.6 tag_date = 0 diff --git a/setup.py b/setup.py index 4cb6a45..e1251c1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ :copyright: (c) 2022-2023 co0lc0der """ -version = '0.3.5' +version = '0.3.6' with open('README.md', encoding='utf-8') as f: long_description = f.read() diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index 09f2b32..a0e2cb4 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -7,5 +7,5 @@ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.5' +__version__ = '0.3.6' __email__ = 'c0der@ya.ru' From 8112e072b036064d2f94f7ec11fd8da20e7c733c Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Tue, 12 Nov 2024 16:48:14 +0500 Subject: [PATCH 50/83] move MetaSingleton and DataBase classes into database.py --- simple_query_builder/__init__.py | 2 +- simple_query_builder/database.py | 35 ++++++++++++++++++++++++++++ simple_query_builder/querybuilder.py | 32 ++----------------------- 3 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 simple_query_builder/database.py diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index a0e2cb4..5d061eb 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -1,7 +1,7 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022-2023 co0lc0der +:copyright: (c) 2022-2024 co0lc0der """ from .querybuilder import * diff --git a/simple_query_builder/database.py b/simple_query_builder/database.py new file mode 100644 index 0000000..682cc3e --- /dev/null +++ b/simple_query_builder/database.py @@ -0,0 +1,35 @@ +""" +:authors: co0lc0der +:license: MIT +:copyright: (c) 2022-2024 co0lc0der +""" + +import sqlite3 + + +class MetaSingleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class DataBase(metaclass=MetaSingleton): + db_name = "db.db" + conn = None + cursor = None + + def connect(self, db_name=""): + if db_name != "": + self.db_name = db_name + + if self.conn is None: + self.conn = sqlite3.connect(self.db_name) + self.cursor = self.conn.cursor() + + return self.conn + + def c(self): + return self.cursor diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index e90adf8..88b2269 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -1,42 +1,14 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022-2023 co0lc0der +:copyright: (c) 2022-2024 co0lc0der """ import inspect -import sqlite3 import sys import traceback from typing import Union - - -class MetaSingleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - - -class DataBase(metaclass=MetaSingleton): - db_name = "db.db" - conn = None - cursor = None - - def connect(self, db_name=""): - if db_name != "": - self.db_name = db_name - - if self.conn is None: - self.conn = sqlite3.connect(self.db_name) - self.cursor = self.conn.cursor() - - return self.conn - - def c(self): - return self.cursor +from database import * class QueryBuilder: From 54f667d7ce33cd1c8720cd960488b97de6c85aa6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 13 Nov 2024 11:14:18 +0500 Subject: [PATCH 51/83] add URI parameter --- simple_query_builder/database.py | 4 +++- simple_query_builder/querybuilder.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/simple_query_builder/database.py b/simple_query_builder/database.py index 682cc3e..8922cef 100644 --- a/simple_query_builder/database.py +++ b/simple_query_builder/database.py @@ -23,10 +23,12 @@ class DataBase(metaclass=MetaSingleton): def connect(self, db_name=""): if db_name != "": + def connect(self, db_name: str = "", uri: bool = False): + if db_name: self.db_name = db_name if self.conn is None: - self.conn = sqlite3.connect(self.db_name) + self.conn = sqlite3.connect(self.db_name, uri=uri) self.cursor = self.conn.cursor() return self.conn diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 88b2269..a81e387 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -56,8 +56,9 @@ class QueryBuilder: _count: int = -1 _params: tuple = () - def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, print_errors: bool = False) -> None: - self._conn = database.connect(db_name) + def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, + print_errors: bool = False, uri: bool = False) -> None: + self._conn = database.connect(db_name, uri) self._print_errors = print_errors self._set_row_factory(result_dict) self._cur = self._conn.cursor() From 5173988fb5ef1700061147d8cdbc4efee568e557 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 14 Nov 2024 12:01:14 +0500 Subject: [PATCH 52/83] add math operators for fields --- simple_query_builder/querybuilder.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index a81e387..4893356 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -24,6 +24,15 @@ class QueryBuilder: "IN", "NOT IN", ] + _MATH_OPERATORS: list = [ + "+", + "-", + "*", + "/", + "%", + "(", + ")", + ] _LOGICS: list = [ "AND", "OR", @@ -190,11 +199,8 @@ def column(self, column: Union[str, int] = 0): return self._result def pluck(self, key: Union[str, int] = 0, column: Union[str, int] = 1): - if ( - self._result_dict and (isinstance(key, int) or isinstance(column, int)) - ) or ( - not self._result_dict and (isinstance(key, str) or isinstance(column, str)) - ): + if (self._result_dict and (isinstance(key, int) or isinstance(column, int))) or\ + (not self._result_dict and (isinstance(key, str) or isinstance(column, str))): self.set_error(f"Incorrect type of key or column in {inspect.stack()[0][3]} method. Result dict is {self._result_dict}") return self @@ -440,7 +446,7 @@ def _prepare_field(self, field: str = "") -> str: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return "" - if field.find("(") > -1 or field.find(")") > -1 or field.find("*") > -1: + if any(x in field for x in self._MATH_OPERATORS): if field.find(" AS ") > -1: field = field.replace(" AS ", " AS `") return f"{field}`" From 1c5d0801f14d9a5b4caa91ab9e7b3aabd558bd54 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 14 Nov 2024 12:02:58 +0500 Subject: [PATCH 53/83] add has_error() method, copies of get_error() method for now --- simple_query_builder/querybuilder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 4893356..9544ff8 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -146,9 +146,13 @@ def get_sql(self) -> str: sql = sql.replace("?", str(p), 1) return sql + # Logics of this method will be changed in next version def get_error(self) -> bool: return self._error + def has_error(self) -> bool: + return self._error + def get_error_message(self) -> str: if self._print_errors and self._error: print(self._error_message) From 9439a2f3891839664b1e4ad11094ec5b8fb8793d Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 14 Nov 2024 12:05:43 +0500 Subject: [PATCH 54/83] add _fields property --- simple_query_builder/querybuilder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 9544ff8..dd5f1c3 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -64,6 +64,7 @@ class QueryBuilder: _result_dict = True _count: int = -1 _params: tuple = () + _fields = [] def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, print_errors: bool = False, uri: bool = False) -> None: @@ -176,6 +177,7 @@ def get_count(self) -> int: def reset(self) -> bool: self._sql = "" self._params = () + self._fields = [] self._query = None self._result = [] self._count = -1 @@ -526,6 +528,8 @@ def insert(self, table: Union[str, dict], fields: Union[list, dict]): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self + self._fields = fields + if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: @@ -561,6 +565,8 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self + self._fields = fields + if isinstance(table, dict) or isinstance(table, str): table = self._prepare_aliases(table) else: From 1b6bf6236bb11634c07b3052cbc81f2135fb28a1 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 14 Nov 2024 12:07:43 +0500 Subject: [PATCH 55/83] add _concat property, some refactor of select() --- simple_query_builder/querybuilder.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index dd5f1c3..2da8fdb 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -57,6 +57,7 @@ class QueryBuilder: _cur = None _query = None _sql: str = "" + _concat = False _error: bool = False _error_message: str = "" _print_errors: bool = False @@ -182,6 +183,7 @@ def reset(self) -> bool: self._result = [] self._count = -1 self.set_error() + self._concat = False return True def all(self) -> Union[tuple, list, dict, None]: @@ -334,10 +336,13 @@ def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self - self.reset() - if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): - self._sql = f"SELECT {self._prepare_aliases(fields)}" + if self._concat: + self._sql += f"SELECT {self._prepare_aliases(fields)}" + else: + self.reset() + self._sql = f"SELECT {self._prepare_aliases(fields)}" + self._fields = fields else: self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self From 0353f3c4eef48872499481c5c20646ac3d62e886 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 14 Nov 2024 12:08:15 +0500 Subject: [PATCH 56/83] add union() and union_select() methods --- simple_query_builder/querybuilder.py | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 2da8fdb..0f00181 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -624,6 +624,38 @@ def join(self, table: Union[str, dict] = "", on: Union[str, tuple, list] = (), j self.set_error() return self + def union(self, union_all: bool = False): + self._concat = True + self._sql += " UNION ALL " if union_all else " UNION " + return self + + def union_select(self, table: Union[str, dict], union_all: bool = False): + if not table: + self.set_error(f"Empty table in {inspect.stack()[0][3]} method") + return self + + if 'UNION' in self._sql: + self.set_error(f"SQL has already UNION in {inspect.stack()[0][3]} method") + return self + + self._concat = True + fields = self._fields + self._sql += " UNION ALL " if union_all else " UNION " + + if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): + self._sql += f"SELECT {self._prepare_aliases(fields)}" + else: + self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") + return self + + if isinstance(table, dict) or isinstance(table, str): + self._sql += f" FROM {self._prepare_aliases(table)}" + else: + self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") + return self + + return self + def drop(self, table: str, add_exists: bool = True): # this method will be moved to another class if not table: From a19f05396e7d069d754baf762bea788fabde4de3 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 15 Nov 2024 12:48:50 +0500 Subject: [PATCH 57/83] rename _OPERATORS property --- simple_query_builder/querybuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 0f00181..375dc6d 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -12,7 +12,7 @@ class QueryBuilder: - _OPERATORS: list = [ + _COND_OPERATORS: list = [ "=", ">", "<", @@ -308,7 +308,7 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: field = self._prepare_field(cond[0]) operator = cond[1].upper() value = cond[2] - if operator in self._OPERATORS: + if operator in self._COND_OPERATORS: if operator == "IN" and ( isinstance(value, list) or isinstance(value, tuple) ): From 3dc52de5c63c74505d0647a2c8e6660e9732be4c Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 15 Nov 2024 12:52:11 +0500 Subject: [PATCH 58/83] add with_values param for get_sql() method --- simple_query_builder/querybuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 375dc6d..76212f4 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -136,10 +136,10 @@ def add_semicolon(self, sql: str = "") -> str: return new_sql - def get_sql(self) -> str: + def get_sql(self, with_values: bool = True) -> str: sql = self._sql params = self._params - if params: + if params and with_values: # Replace ? with markers for p in params: if isinstance(p, str): From 5d3d6c2c7972d45408e1bd2c6f0d8fd9f16c7c7f Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 15 Nov 2024 15:50:06 +0500 Subject: [PATCH 59/83] add __str__() method --- simple_query_builder/querybuilder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 76212f4..bcd51bd 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -656,6 +656,9 @@ def union_select(self, table: Union[str, dict], union_all: bool = False): return self + def __str__(self): + return self.get_sql() + def drop(self, table: str, add_exists: bool = True): # this method will be moved to another class if not table: From 3b0305f4aa7c99c9f68106277dc4ac2c77b81b51 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 15 Nov 2024 15:51:25 +0500 Subject: [PATCH 60/83] rename _MATH_OPERATORS property and improve select() method --- simple_query_builder/querybuilder.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index bcd51bd..34133f5 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -19,12 +19,13 @@ class QueryBuilder: ">=", "<=", "!=", + "<>", "LIKE", "NOT LIKE", "IN", "NOT IN", ] - _MATH_OPERATORS: list = [ + _FIELD_SPEC_CHARS: list = [ "+", "-", "*", @@ -32,6 +33,7 @@ class QueryBuilder: "%", "(", ")", + "||", ] _LOGICS: list = [ "AND", @@ -348,7 +350,11 @@ def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): return self if isinstance(table, dict) or isinstance(table, str): - self._sql += f" FROM {self._prepare_aliases(table)}" + if isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS) and fields == '*': + self._sql = f"SELECT {table}" + self._fields = table + else: + self._sql += f" FROM {self._prepare_aliases(table)}" else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self @@ -457,7 +463,7 @@ def _prepare_field(self, field: str = "") -> str: self.set_error(f"Empty field in {inspect.stack()[0][3]} method") return "" - if any(x in field for x in self._MATH_OPERATORS): + if any(x in field for x in self._FIELD_SPEC_CHARS): if field.find(" AS ") > -1: field = field.replace(" AS ", " AS `") return f"{field}`" From 2e37bce998692191c7fafb2d6fb60ef071f61f8f Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Tue, 19 Nov 2024 14:55:15 +0500 Subject: [PATCH 61/83] add _prepare_tables() method --- simple_query_builder/querybuilder.py | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 34133f5..9bfd32c 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -257,11 +257,7 @@ def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) elif isinstance(item, dict): first_item = list(item.values())[0] alias = list(item.keys())[0] - sql.append( - first_item - if isinstance(alias, int) - else f"{first_item} AS {alias}" - ) + sql.append(first_item if isinstance(alias, int) else f"{first_item} AS {alias}") elif isinstance(items, dict): new_item = items[item] sql.append(new_item if isinstance(item, int) else f"{new_item} AS {item}") @@ -333,7 +329,17 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: return result - def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): + def _prepare_tables(self, table: Union[str, list, dict]) -> str: + if isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS): + self._sql = f"SELECT {table}" + self._fields = table + elif isinstance(table, str) and 'select' in table.lower(): + self._sql += f" FROM ({table})" + else: + self._sql += f" FROM {self._prepare_aliases(table)}" + return self._sql + + def select(self, table: Union[str, list, dict], fields: Union[str, list, dict] = "*"): if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self @@ -349,12 +355,8 @@ def select(self, table: Union[str, dict], fields: Union[str, list, dict] = "*"): self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") return self - if isinstance(table, dict) or isinstance(table, str): - if isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS) and fields == '*': - self._sql = f"SELECT {table}" - self._fields = table - else: - self._sql += f" FROM {self._prepare_aliases(table)}" + if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): + self._prepare_tables(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self @@ -518,30 +520,29 @@ def group_by(self, field: Union[str, tuple, list] = ()): return self - def delete(self, table: Union[str, dict]): + def delete(self, table: Union[str, list, dict]): if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict) or isinstance(table, str): - table = self._prepare_aliases(table) + if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): + self.reset() + self._sql = f"DELETE" + self._prepare_tables(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") return self - self.reset() - - self._sql = f"DELETE FROM {table}" return self - def insert(self, table: Union[str, dict], fields: Union[list, dict]): + def insert(self, table: Union[str, list, dict], fields: Union[list, dict]): if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self self._fields = fields - if isinstance(table, dict) or isinstance(table, str): + if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): table = self._prepare_aliases(table) else: self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") From 407eef2e875145fdc5538ae33117fed3dd128b12 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 22 Nov 2024 13:59:49 +0500 Subject: [PATCH 62/83] refactor delete() method --- simple_query_builder/querybuilder.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 9bfd32c..3f78e4a 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -525,13 +525,8 @@ def delete(self, table: Union[str, list, dict]): self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): - self.reset() - self._sql = f"DELETE" - self._prepare_tables(table) - else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") - return self + self.reset() + self._sql = f"DELETE FROM {self._prepare_tables(table)}" return self From 49be64c09dc393ab0a372c8ea494c6d32a6da9b7 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 22 Nov 2024 14:01:22 +0500 Subject: [PATCH 63/83] refactor insert() method --- simple_query_builder/querybuilder.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 3f78e4a..096d725 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -535,15 +535,9 @@ def insert(self, table: Union[str, list, dict], fields: Union[list, dict]): self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self - self._fields = fields - - if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): - table = self._prepare_aliases(table) - else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") - return self - self.reset() + table = self._prepare_aliases(table) + self._fields = fields if isinstance(fields, dict): values = ("?," * len(fields)).rstrip(",") From d7d45a5430b3caa198bf78f28213b2cc1b771ef2 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Fri, 22 Nov 2024 14:01:47 +0500 Subject: [PATCH 64/83] refactor _prepare_tables() method --- simple_query_builder/querybuilder.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 096d725..7444fae 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -330,14 +330,18 @@ def _prepare_conditions(self, where: Union[str, list]) -> dict: return result def _prepare_tables(self, table: Union[str, list, dict]) -> str: - if isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS): - self._sql = f"SELECT {table}" - self._fields = table - elif isinstance(table, str) and 'select' in table.lower(): - self._sql += f" FROM ({table})" - else: - self._sql += f" FROM {self._prepare_aliases(table)}" - return self._sql + if not table: + self.set_error(f"Empty table in {inspect.stack()[0][3]} method") + return '' + + if isinstance(table, str) and 'select' in table.lower(): + self._concat = True + return f"({table})" + elif isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS): + # self._fields = table + return f"{table}" + + return self._prepare_aliases(table) def select(self, table: Union[str, list, dict], fields: Union[str, list, dict] = "*"): if not table or not fields: From 9667cae199c672852690930168b726d567d41bb6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Tue, 26 Nov 2024 15:16:49 +0500 Subject: [PATCH 65/83] add drop_view() method --- simple_query_builder/querybuilder.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 7444fae..4d70709 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -657,7 +657,20 @@ def union_select(self, table: Union[str, dict], union_all: bool = False): return self def __str__(self): - return self.get_sql() + return self.get_sql(False) + + def drop_view(self, view_name: str, add_exists: bool = True): + # this method will be moved to another class + if not view_name: + self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") + return self + + exists = "IF EXISTS " if add_exists else "" + + self.reset() + self._sql = f"DROP VIEW {exists}`{view_name}`" + + return self def drop(self, table: str, add_exists: bool = True): # this method will be moved to another class From 2be8d57c4645e0904b966b8aeb1a84e54987eded Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Tue, 26 Nov 2024 15:17:40 +0500 Subject: [PATCH 66/83] add create_view() method --- simple_query_builder/querybuilder.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 4d70709..4d47a64 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -659,6 +659,22 @@ def union_select(self, table: Union[str, dict], union_all: bool = False): def __str__(self): return self.get_sql(False) + def create_view(self, view_name: str, add_exists: bool = True): + # this method will be moved to another class + if not view_name: + self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") + return self + + exists = "IF NOT EXISTS " if add_exists else "" + + # self.reset() + if 'select' not in self._sql.lower(): + self.set_error(f"No select found in {inspect.stack()[0][3]} method") + return self + self._sql = f"CREATE VIEW {exists}`{view_name}` AS " + self._sql + + return self + def drop_view(self, view_name: str, add_exists: bool = True): # this method will be moved to another class if not view_name: From 368881758514f122515cb926a0ebdb98236243ac Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 18 Dec 2024 16:06:16 +0500 Subject: [PATCH 67/83] add comment for get_error() method --- simple_query_builder/querybuilder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 4d47a64..6da8770 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -150,8 +150,10 @@ def get_sql(self, with_values: bool = True) -> str: sql = sql.replace("?", str(p), 1) return sql - # Logics of this method will be changed in next version def get_error(self) -> bool: + """ + Logics of this method will be changed in next version, use has_error() instead + """ return self._error def has_error(self) -> bool: From 8b11f56b0bf5760615b283306a068887dde48a0e Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 18 Dec 2024 16:43:30 +0500 Subject: [PATCH 68/83] refactor select() method --- simple_query_builder/querybuilder.py | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 6da8770..4198ca4 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -345,27 +345,30 @@ def _prepare_tables(self, table: Union[str, list, dict]) -> str: return self._prepare_aliases(table) - def select(self, table: Union[str, list, dict], fields: Union[str, list, dict] = "*"): + def select(self, table: Union[str, list, dict], fields: Union[str, list, dict] = "*", dist: bool = False): if not table or not fields: self.set_error(f"Empty table or fields in {inspect.stack()[0][3]} method") return self - if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): - if self._concat: - self._sql += f"SELECT {self._prepare_aliases(fields)}" - else: - self.reset() - self._sql = f"SELECT {self._prepare_aliases(fields)}" - self._fields = fields + prepared_table = self._prepare_tables(table) + prepared_fields = self._prepare_aliases(fields) + + if not self._concat: + self.reset() + + sql = 'SELECT ' + sql += 'DISTINCT ' if dist else '' + if isinstance(table, str) and any(x in table for x in self._FIELD_SPEC_CHARS) and fields == '*': + sql += f"{prepared_table}" + self._fields = self._prepare_aliases(table) else: - self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") - return self + self._fields = fields + sql += f"{prepared_fields} FROM {prepared_table}" - if isinstance(table, dict) or isinstance(table, list) or isinstance(table, str): - self._prepare_tables(table) + if self._concat: + self._sql += sql else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") - return self + self._sql = sql return self From ee566d431aaaa55faa2e12954597a32a45401c76 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 18 Dec 2024 16:44:18 +0500 Subject: [PATCH 69/83] refactor union_select() method --- simple_query_builder/querybuilder.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 4198ca4..22c917f 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -634,18 +634,18 @@ def union(self, union_all: bool = False): self._sql += " UNION ALL " if union_all else " UNION " return self - def union_select(self, table: Union[str, dict], union_all: bool = False): + def union_select(self, table: Union[str, list, dict], union_all: bool = False): if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if 'UNION' in self._sql: - self.set_error(f"SQL has already UNION in {inspect.stack()[0][3]} method") - return self - self._concat = True - fields = self._fields - self._sql += " UNION ALL " if union_all else " UNION " + fields = self._fields if self._fields else '*' + sql = self._sql + sql += " UNION ALL " if union_all else " UNION " + self._sql = sql + f"SELECT {self._prepare_aliases(fields)} FROM {self._prepare_aliases(table)}" + + return self if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): self._sql += f"SELECT {self._prepare_aliases(fields)}" From b976e078d6fb1a9220918e8dd0b2b129efd4f685 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 18 Dec 2024 16:45:39 +0500 Subject: [PATCH 70/83] add except_select() method --- simple_query_builder/querybuilder.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 22c917f..369ad7d 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -647,17 +647,14 @@ def union_select(self, table: Union[str, list, dict], union_all: bool = False): return self - if isinstance(fields, dict) or isinstance(fields, list) or isinstance(fields, str): - self._sql += f"SELECT {self._prepare_aliases(fields)}" - else: - self.set_error(f"Incorrect type of fields in {inspect.stack()[0][3]} method. Fields must be String, List or Dictionary") + def except_select(self, table: Union[str, list, dict]): + if not table: + self.set_error(f"Empty table in {inspect.stack()[0][3]} method") return self - if isinstance(table, dict) or isinstance(table, str): - self._sql += f" FROM {self._prepare_aliases(table)}" - else: - self.set_error(f"Incorrect type of table in {inspect.stack()[0][3]} method. Table must be String or Dictionary") - return self + self._concat = True + fields = self._fields if self._fields else '*' + self._sql += f" EXCEPT SELECT {self._prepare_aliases(fields)} FROM {self._prepare_aliases(table)}" return self From fd18dca91a672ac4dfb6830faaa7b79a2d87cc2d Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 09:37:13 +0500 Subject: [PATCH 71/83] add excepts() method --- simple_query_builder/querybuilder.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 369ad7d..f548f08 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -647,6 +647,11 @@ def union_select(self, table: Union[str, list, dict], union_all: bool = False): return self + def excepts(self): + self._concat = True + self._sql += " EXCEPT " + return self + def except_select(self, table: Union[str, list, dict]): if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") @@ -662,7 +667,6 @@ def __str__(self): return self.get_sql(False) def create_view(self, view_name: str, add_exists: bool = True): - # this method will be moved to another class if not view_name: self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") return self @@ -678,7 +682,6 @@ def create_view(self, view_name: str, add_exists: bool = True): return self def drop_view(self, view_name: str, add_exists: bool = True): - # this method will be moved to another class if not view_name: self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") return self From 4bccfff4692b8b61d4d8c0bc6c9e2e193cb9e53d Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 10:49:05 +0500 Subject: [PATCH 72/83] add intersect() method --- simple_query_builder/querybuilder.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index f548f08..a36b344 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -663,6 +663,11 @@ def except_select(self, table: Union[str, list, dict]): return self + def intersect(self): + self._concat = True + self._sql += " INTERSECT " + return self + def __str__(self): return self.get_sql(False) From 0959f64bee9f2e2a65a5da61f816dfd2e6924d83 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 10:49:40 +0500 Subject: [PATCH 73/83] add intersect_select() method --- simple_query_builder/querybuilder.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index a36b344..31b79fa 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -668,6 +668,17 @@ def intersect(self): self._sql += " INTERSECT " return self + def intersect_select(self, table: Union[str, list, dict]): + if not table: + self.set_error(f"Empty table in {inspect.stack()[0][3]} method") + return self + + self._concat = True + fields = self._fields if self._fields else '*' + self._sql += f" INTERSECT SELECT {self._prepare_aliases(fields)} FROM {self._prepare_aliases(table)}" + + return self + def __str__(self): return self.get_sql(False) From d170d707412b49e16ca67e2ba4159c368b861bb6 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 13:27:50 +0500 Subject: [PATCH 74/83] add _SQLITE_JOIN_TYPES property --- simple_query_builder/querybuilder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 31b79fa..91b3abf 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -51,6 +51,11 @@ class QueryBuilder: "FULL OUTER", "CROSS" ] + _SQLITE_JOIN_TYPES: list = [ + "INNER", + "LEFT OUTER", + "CROSS" + ] _NO_FETCH: int = 0 _FETCH_ONE: int = 1 _FETCH_ALL: int = 2 @@ -601,7 +606,7 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): def join(self, table: Union[str, dict] = "", on: Union[str, tuple, list] = (), join_type: str = "INNER"): join_type = join_type.upper() - if join_type == "" or join_type not in self._JOIN_TYPES: + if join_type == "" or join_type not in self._SQLITE_JOIN_TYPES: self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method") return self From e1038e6b83d9982f49c7783448aba25f27cd33a9 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 13:37:25 +0500 Subject: [PATCH 75/83] add driver property for Database --- simple_query_builder/database.py | 12 +++++++----- simple_query_builder/querybuilder.py | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/simple_query_builder/database.py b/simple_query_builder/database.py index 8922cef..f9e6828 100644 --- a/simple_query_builder/database.py +++ b/simple_query_builder/database.py @@ -17,19 +17,21 @@ def __call__(cls, *args, **kwargs): class DataBase(metaclass=MetaSingleton): - db_name = "db.db" + driver = "sqlite" + db_name = ":memory:" conn = None cursor = None - def connect(self, db_name=""): - if db_name != "": def connect(self, db_name: str = "", uri: bool = False): if db_name: self.db_name = db_name if self.conn is None: - self.conn = sqlite3.connect(self.db_name, uri=uri) - self.cursor = self.conn.cursor() + if self.driver == "sqlite": + self.conn = sqlite3.connect(self.db_name, uri=uri) + self.cursor = self.conn.cursor() + else: + print("Wrong DB driver. At present time it's supported 'sqlite' only") return self.conn diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index 91b3abf..aa2a3bf 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -212,7 +212,7 @@ def column(self, column: Union[str, int] = 0): self.set_error(f"Incorrect type of column in {inspect.stack()[0][3]} method. Result dict is {self._result_dict}") return self - self.query("", (), self._FETCH_COLUMN, column) + self.query(fetch=self._FETCH_COLUMN, column=column) return self._result def pluck(self, key: Union[str, int] = 0, column: Union[str, int] = 1): @@ -245,7 +245,7 @@ def get_last(self): return self._result[-1] def exists(self) -> bool: - result = self.one() + self.one() return self._count > 0 def _prepare_aliases(self, items: Union[str, list, dict], as_list: bool = False) -> Union[str, list]: @@ -688,6 +688,7 @@ def __str__(self): return self.get_sql(False) def create_view(self, view_name: str, add_exists: bool = True): + # this method will be moved to another class if not view_name: self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") return self @@ -703,6 +704,7 @@ def create_view(self, view_name: str, add_exists: bool = True): return self def drop_view(self, view_name: str, add_exists: bool = True): + # this method will be moved to another class if not view_name: self.set_error(f"Empty view_name in {inspect.stack()[0][3]} method") return self From e2c93b1f9f5e299372529c9711a76711a7fd62df Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 13:47:22 +0500 Subject: [PATCH 76/83] rename properties in Database class, add get_driver() method --- simple_query_builder/database.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/simple_query_builder/database.py b/simple_query_builder/database.py index f9e6828..41d3923 100644 --- a/simple_query_builder/database.py +++ b/simple_query_builder/database.py @@ -17,23 +17,26 @@ def __call__(cls, *args, **kwargs): class DataBase(metaclass=MetaSingleton): - driver = "sqlite" - db_name = ":memory:" - conn = None - cursor = None + _driver = "sqlite" + _db_name = ":memory:" + _conn = None + _cursor = None def connect(self, db_name: str = "", uri: bool = False): if db_name: - self.db_name = db_name + self._db_name = db_name - if self.conn is None: - if self.driver == "sqlite": - self.conn = sqlite3.connect(self.db_name, uri=uri) - self.cursor = self.conn.cursor() + if self._conn is None: + if self._driver == "sqlite": + self._conn = sqlite3.connect(self._db_name, uri=uri) + self._cursor = self._conn.cursor() else: print("Wrong DB driver. At present time it's supported 'sqlite' only") - return self.conn + return self._conn def c(self): - return self.cursor + return self._cursor + + def get_driver(self): + return self._driver From 7024acfcf757d4cd8526fa1545566e452562f36b Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Thu, 19 Dec 2024 13:51:33 +0500 Subject: [PATCH 77/83] add _db property, refactor constructor in QueryBuilder --- simple_query_builder/querybuilder.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index aa2a3bf..b4dab19 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -60,6 +60,7 @@ class QueryBuilder: _FETCH_ONE: int = 1 _FETCH_ALL: int = 2 _FETCH_COLUMN: int = 3 + _db = None _conn = None _cur = None _query = None @@ -76,10 +77,14 @@ class QueryBuilder: def __init__(self, database: DataBase, db_name: str = "", result_dict: bool = True, print_errors: bool = False, uri: bool = False) -> None: - self._conn = database.connect(db_name, uri) + if database: + self._db = database + self._conn = self._db.connect(db_name, uri) + self._set_row_factory(result_dict) + self._cur = self._conn.cursor() + else: + self.set_error(f"Empty database in {inspect.stack()[0][3]} method") self._print_errors = print_errors - self._set_row_factory(result_dict) - self._cur = self._conn.cursor() def _set_row_factory(self, result_dict: bool = True): self._result_dict = result_dict From b54b784666c0f5a71b5a24d0941648b899460411 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 11:11:12 +0500 Subject: [PATCH 78/83] refactor join() method --- simple_query_builder/querybuilder.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index b4dab19..d9ccc5a 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -46,6 +46,7 @@ class QueryBuilder: ] _JOIN_TYPES: list = [ "INNER", + "LEFT", "LEFT OUTER", "RIGHT OUTER", "FULL OUTER", @@ -53,6 +54,7 @@ class QueryBuilder: ] _SQLITE_JOIN_TYPES: list = [ "INNER", + "LEFT", "LEFT OUTER", "CROSS" ] @@ -178,7 +180,7 @@ def set_error(self, message: str = "") -> None: self._error = bool(message) self._error_message = message if self._print_errors and self._error: - print(self._error_message) + print("QueryBuilder error:", self._error_message) def get_params(self) -> tuple: return self._params @@ -611,9 +613,14 @@ def update(self, table: Union[str, dict], fields: Union[list, dict]): def join(self, table: Union[str, dict] = "", on: Union[str, tuple, list] = (), join_type: str = "INNER"): join_type = join_type.upper() - if join_type == "" or join_type not in self._SQLITE_JOIN_TYPES: - self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method") - return self + if self._db.get_driver() == 'sqlite': + if join_type == "" or join_type not in self._SQLITE_JOIN_TYPES: + self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method. Try one of these {self._SQLITE_JOIN_TYPES}") + return self + else: + if join_type == "" or join_type not in self._JOIN_TYPES: + self.set_error(f"Empty join_type or is not allowed in {inspect.stack()[0][3]} method. Try one of these {self._JOIN_TYPES}") + return self if not table: self.set_error(f"Empty table in {inspect.stack()[0][3]} method") From 7f4b61c18e1064593257222190ea6374199a9b1c Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 11:11:35 +0500 Subject: [PATCH 79/83] fix limit() method --- simple_query_builder/querybuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simple_query_builder/querybuilder.py b/simple_query_builder/querybuilder.py index d9ccc5a..7672103 100644 --- a/simple_query_builder/querybuilder.py +++ b/simple_query_builder/querybuilder.py @@ -462,7 +462,8 @@ def not_null(self, field: str): return self def limit(self, limit: int = 1): - self._sql += f" LIMIT {limit}" + if 'DELETE' not in self._sql: + self._sql += f" LIMIT {limit}" return self def offset(self, offset: int = 0): From bbe5ad9aca3205c403694fd19f833758efc56feb Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 11:14:01 +0500 Subject: [PATCH 80/83] improve docs --- README.md | 341 +++------------------------------------------ ToDo.md | 47 +++++++ docs/Delete.md | 37 +++++ docs/Init.md | 21 +++ docs/Insert.md | 33 +++++ docs/Install.md | 16 +++ docs/Methods.md | 26 ++++ docs/Select.md | 186 +++++++++++++++++++++++++ docs/Select_ext.md | 186 +++++++++++++++++++++++++ docs/Table.md | 35 +++++ docs/Update.md | 41 ++++++ docs/View.md | 55 ++++++++ docs/index.md | 20 +++ 13 files changed, 726 insertions(+), 318 deletions(-) create mode 100644 ToDo.md create mode 100644 docs/Delete.md create mode 100644 docs/Init.md create mode 100644 docs/Insert.md create mode 100644 docs/Install.md create mode 100644 docs/Methods.md create mode 100644 docs/Select.md create mode 100644 docs/Select_ext.md create mode 100644 docs/Table.md create mode 100644 docs/Update.md create mode 100644 docs/View.md create mode 100644 docs/index.md diff --git a/README.md b/README.md index 5311f29..c9c9eb4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) ![GitHub repo size](https://img.shields.io/github/repo-size/co0lc0der/simple-query-builder-python?label=size&style=flat-square) [![GitHub license](https://img.shields.io/github/license/co0lc0der/simple-query-builder-python?style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/blob/main/LICENSE.md) -![Python 3.7, 3.8, 3.9, 3.10](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) +![Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) ![PyPI](https://img.shields.io/pypi/v/simple-query-builder?color=yellow&style=flat-square) ![PyPI - Downloads](https://img.shields.io/pypi/dm/simple-query-builder?color=darkgreen&style=flat-square) @@ -30,278 +30,33 @@ Or from Github: pip install https://github.com/co0lc0der/simple-query-builder-python/archive/main.zip ``` ## How to use -### Main public methods -- `get_sql()` returns SQL query string which will be executed -- `get_params()` returns a tuple of parameters for a query -- `get_result()` returns query's result -- `get_count()` returns result's rows count -- `get_error()` returns `True` if an error is had -- `get_error_message()` returns an error message if an error is had -- `set_error(message)` sets `_error` to `True` and `_error_message` -- `get_first()` returns the first item of results -- `get_last()` returns the last item of results -- `reset()` resets state to default values -- `all()` executes SQL query and returns all rows of result (`fetchall()`) -- `one()` executes SQL query and returns the first row of result (`fetchone()`) -- `column(col)` executes SQL query and returns the needed column of result by its index or name, `col` is `0` by default -- `pluck(key, col)` executes SQL query and returns a list of tuples/dicts (the key (usually ID) and the needed column of result) by its indexes or names, `key` is `0` and `col` is `1` by default -- `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) -- `exists()` returns `True` if SQL query has a row and `False` if it hasn't -- `count()` prepares a query with SQL `COUNT(*)` function and executes it -- `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries -- 'SQL' methods are presented in [Usage section](#usage-examples) - ### Import the module and init `QueryBuilder` with `Database()` ```python from simple_query_builder import * -# if you want to get results as a list of dictionaries (by default since 0.3.5) -qb = QueryBuilder(DataBase(), 'my_db.db') # result_dict=True, print_errors=False - -# or if you want to get results as a list of tuples (since 0.3.5) -qb = QueryBuilder(DataBase(), 'my_db.db', result_dict=False) +qb = QueryBuilder(DataBase(), 'my_db.db') -# for printing errors into terminal (since 0.3.5) -qb = QueryBuilder(DataBase(), 'my_db.db', print_errors=True) +# or DB in memory +qb = QueryBuilder(DataBase(), ':memory:') ``` ### Usage examples -- Select all rows from a table +#### Select all rows from a table ```python results = qb.select('users').all() ``` +Result query ```sql SELECT * FROM `users`; ``` -- Select a row with a condition -```python -results = qb.select('users').where([['id', '=', 10]]).one() -# or since 0.3.4 -results = qb.select('users').where([['id', 10]]).one() -``` -```sql -SELECT * FROM `users` WHERE `id` = 10; -``` -- Select rows with two conditions +#### Select rows with two conditions ```python results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', '=', 2]]).all() -# or since 0.3.4 -results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', 2]]).all() ``` +Result query ```sql SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); ``` -- Select a row with a `LIKE` and `NOT LIKE` condition -```python -results = qb.select('users').like(['name', '%John%']).all() -# or -results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() -# or since 0.3.5 -results = qb.select('users').like('name', '%John%').all() -``` -```sql -SELECT * FROM `users` WHERE (`name` LIKE '%John%'); -``` -```python -results = qb.select('users').not_like(['name', '%John%']).all() -# or -results = qb.select('users').where([['name', 'NOT LIKE', '%John%']]).all() -# or since 0.3.5 -results = qb.select('users').not_like('name', '%John%').all() -``` -```sql -SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%'); -``` -- Select a row with a `IS NULL` and `IS NOT NULL` condition (since 0.3.5) -```python -results = qb.select('users').is_null('phone').all() -# or -results = qb.select('users').where([['phone', 'is null']]).all() -``` -```sql -SELECT * FROM `users` WHERE (`phone` IS NULL); -``` -```python -results = qb.select('customers').is_not_null('address').all() -# or -results = qb.select('customers').not_null('address').all() -# or -results = qb.select('customers').where([['address', 'is not null']]).all() -``` -```sql -SELECT * FROM `customers` WHERE (`address` IS NOT NULL); -``` -- Select rows with `OFFSET` and `LIMIT` -```python -results = qb.select('posts')\ - .where([['user_id', '=', 3]])\ - .offset(14)\ - .limit(7)\ - .all() -# or since 0.3.4 -results = qb.select('posts')\ - .where([['user_id', 3]])\ - .offset(14)\ - .limit(7)\ - .all() -``` -```sql -SELECT * FROM `posts` WHERE (`user_id` = 3) OFFSET 14 LIMIT 7; -``` -- Select custom fields with additional SQL -1. `COUNT()` -```python -results = qb.select('users', {'counter': 'COUNT(*)'}).one() -# or -results = qb.count('users').one() -``` -```sql -SELECT COUNT(*) AS `counter` FROM `users`; -``` -2. `ORDER BY` -```python -results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ - .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ - .order_by('b.id', 'desc')\ - .all() -# or since 0.3.4 -results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ - .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ - .order_by('b.id desc')\ - .all() -``` -```sql -SELECT `b`.`id`, `b`.`name` FROM `branches` AS `b` -WHERE (`b`.`id` > 1) AND (`b`.`parent_id` = 1) -ORDER BY `b`.`id` DESC; -``` -3. `GROUP BY` and `HAVING` -```python -results = qb.select('posts', ['id', 'category', 'title'])\ - .where([['views', '>=', 1000]])\ - .group_by('category')\ - .all() -``` -```sql -SELECT `id`, `category`, `title` FROM `posts` -WHERE (`views` >= 1000) GROUP BY `category`; -``` -```python -groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ - .where([['YEAR(`created_at`)', '=', 2020]])\ - .group_by('month_num')\ - .having([['total', '=', 20000]])\ - .all() -# or since 0.3.4 -groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ - .where([['YEAR(`created_at`)', 2020]])\ - .group_by('month_num')\ - .having([['total', 20000]])\ - .all() -``` -```sql -SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` -FROM `orders` WHERE (YEAR(`created_at`) = 2020) -GROUP BY `month_num` HAVING (`total` = 20000); -``` -4. `JOIN`. Supports `INNER`, `LEFT OUTER`, `RIGHT OUTER`, `FULL OUTER` and `CROSS` joins (`INNER` is by default) -```python -results = qb.select({'u': 'users'}, [ - 'u.id', - 'u.email', - 'u.username', - {'perms': 'groups.permissions'} - ])\ - .join('groups', ['u.group_id', 'groups.id'])\ - .limit(5)\ - .all() -``` -```sql -SELECT `u`.`id`, `u`.`email`, `u`.`username`, `groups`.`permissions` AS `perms` -FROM `users` AS `u` -INNER JOIN `groups` ON `u`.`group_id` = `groups`.`id` -LIMIT 5; -``` -```python -results = qb.select({'cp': 'cabs_printers'}, [ - 'cp.id', - 'cp.cab_id', - {'cab_name': 'cb.name'}, - 'cp.printer_id', - {'printer_name': 'p.name'}, - {'cartridge_type': 'c.name'}, - 'cp.comment' - ])\ - .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ - .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ - .join({'c': 'cartridge_types'}, 'p.cartridge_id=c.id')\ - .where([['cp.cab_id', 'in', [11, 12, 13]], 'or', ['cp.cab_id', '=', 5], 'and', ['p.id', '>', 'c.id']])\ - .all() -``` -```sql -SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, - `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` -FROM `cabs_printers` AS `cp` -INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` -INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` -INNER JOIN `cartridge_types` AS `c` ON p.cartridge_id=c.id -WHERE (`cp`.`cab_id` IN (11, 12, 13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`); -``` -```python -# since 0.3.4 -results = qb.select({'cp': 'cabs_printers'}, [ - 'cp.id', - 'cp.cab_id', - {'cab_name': 'cb.name'}, - 'cp.printer_id', - {'cartridge_id': 'c.id'}, - {'printer_name': 'p.name'}, - {'cartridge_type': 'c.name'}, - 'cp.comment' - ])\ - .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ - .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ - .join({'c': 'cartridge_types'}, ['p.cartridge_id', 'c.id'])\ - .group_by(['cp.printer_id', 'cartridge_id'])\ - .order_by(['cp.cab_id', 'cp.printer_id desc'])\ - .all() -``` -```sql -SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, `c`.`id` AS `cartridge_id`, - `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` -FROM `cabs_printers` AS `cp` -INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` -INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` -INNER JOIN `cartridge_types` AS `c` ON `p`.`cartridge_id` = `c`.`id` -GROUP BY `cp`.`printer_id`, `cartridge_id` -ORDER BY `cp`.`cab_id` ASC, `cp`.`printer_id` DESC; -``` -- Insert a row -```python -new_id = qb.insert('groups', { - 'name': 'Moderator', - 'permissions': 'moderator' -}).go() -``` -```sql -INSERT INTO `groups` (`name`, `permissions`) VALUES ('Moderator', 'moderator'); -``` -- Insert many rows -```python -qb.insert('groups', [['name', 'role'], - ['Moderator', 'moderator'], - ['Moderator2', 'moderator'], - ['User', 'user'], - ['User2', 'user'] -]).go() -``` -```sql -INSERT INTO `groups` (`name`, `role`) -VALUES ('Moderator', 'moderator'), - ('Moderator2', 'moderator'), - ('User', 'user'), - ('User2', 'user'); -``` -- Update a row +#### Update a row ```python qb.update('users', { 'username': 'John Doe', @@ -310,72 +65,22 @@ qb.update('users', { .where([['id', '=', 7]])\ .limit()\ .go() -# or since 0.3.4 -qb.update('users', { - 'username': 'John Doe', - 'status': 'new status' - })\ - .where([['id', 7]])\ - .limit()\ - .go() ``` +Result query ```sql UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' WHERE `id` = 7 LIMIT 1; ``` -- Update rows -```python -qb.update('posts', {'status': 'published'})\ - .where([['YEAR(`updated_at`)', '>', 2020]])\ - .go() -``` -```sql -UPDATE `posts` SET `status` = 'published' -WHERE (YEAR(`updated_at`) > 2020); -``` -- Delete a row -```python -qb.delete('users')\ - .where([['name', '=', 'John']])\ - .limit()\ - .go() -# or since 0.3.4 -qb.delete('users')\ - .where([['name', 'John']])\ - .limit()\ - .go() -``` -```sql -DELETE FROM `users` WHERE `name` = 'John' LIMIT 1; -``` -- Delete rows -```python -qb.delete('comments')\ - .where([['user_id', '=', 10]])\ - .go() -# or since 0.3.4 -qb.delete('comments')\ - .where([['user_id', 10]])\ - .go() -``` -```sql -DELETE FROM `comments` WHERE `user_id` = 10; -``` -- Truncate a table - -This method will be moved to another class -```python -qb.truncate('users').go() -``` -```sql -TRUNCATE TABLE `users`; -``` -- Drop a table - -This method will be moved to another class -```python -qb.drop('temporary').go() -``` -```sql -DROP TABLE IF EXISTS `temporary`; -``` +More examples you can find in [documentation](docs/index.md) + +## ToDo +I'm going to add the next features into future versions +- write more unit testes +- add subqueries for QueryBuilder +- add `BETWEEN` +- add `WHERE EXISTS` +- add TableBuilder class (for beginning `CREATE TABLE`, move `qb.drop()` and `qb.truncate()` into it) +- add MySQL support +- add PostgreSQL support +- add `WITH` +- and probably something more diff --git a/ToDo.md b/ToDo.md new file mode 100644 index 0000000..2e79edb --- /dev/null +++ b/ToDo.md @@ -0,0 +1,47 @@ +# TODO +- [x] `order_by()`: добавить возможность указывать несколько полей для сортировки [0.3.4] +- [x] `group_by()`: добавить возможность указывать несколько полей [0.3.4] +- [x] `_prepare_conditions()`: если 2й параметр (вместо условия) не оператор, то оператор `=` [0.3.4] +- [x] `_prepare_conditions()`: если 2й параметр список, то оператор `IN` [0.3.4] +- [x] добавить метод `pluck(id, column)` (как в Laravel) [0.3.4] +- [x] добавить методы `is_null(field)`, `is_not_null(field)` и `not_null(field)` [0.3.5] +- [x] в методах `like()` и `not_like()` можно без `[]` [0.3.5] +- [x] св-во `_print_errors` и параметр в `__init__()` [0.3.5] +- [x] параметр `result_dict` в `QB.__init__()` [0.3.5] +- [x] в методе `get_sql()` `?` меняются на соответствующие значения [0.3.5] +- [x] рефактор методов `query()`, `pluck()` и `column()` [0.3.6] +- [X] вынести в отдельный файл классы `MetaSingleton` и `DataBase` [0.4] +- [X] добавить мат.операторы (и не только) в поля [0.4] +- [X] добавить `UNION` и `UNION ALL` [0.4] +- [X] добавить `DISTINCT` в `SELECT` [0.4] +- [X] добавить `EXCEPT` в `SELECT` [0.4] +- [X] добавить `INTERSECT` в `SELECT` [0.4] +- [X] в методе `get_sql()` параметр `with_values=True` [0.4] +- [X] добавить метод `has_error()` на замену `get_error()` в след.версии [0.4] +- [X] добавить метод `__str__()` [0.4] +- [X] добавить `list` в типы параметров `table` [0.4] +- [X] покрыть тестами SQL методы: `SELECT, INSERT, UPDATE, DELETE` (примеры из README + новые методы) [0.4] +- [X] разделить документацию на файлы [0.4] +- [X] добавить новые фун-ии в документацию [0.4] +- [X] добавить `CREATE VIEW view_name AS SQL_SELECT` [0.4] +- [X] добавить `DROP VIEW view_name` [0.4] +- [ ] в `.where()` можно передавать по 1 или 2 строки вместо списка [0.4?] +- [ ] добавить скобки `()` в выражения (subquery). сложенные выражения (select в select'e), использование в field, from, where, join [0.5?] +- [ ] добавить `WHERE EXISTS` [0.5?] +- [ ] добавить `BETWEEN` [0.5?] +- [ ] добавить `WITH` [0.5?] +- [ ] создать **класс `TableBuilder`** для манипуляций с таблицами [0.5?] +- [ ] перенести в **`TableBuilder`** методы `qb.drop()` и `qb.truncate()` [0.5?] +- [ ] перенести в **`TableBuilder`** методы `qb.create_view()` и `qb.drop_view()` [0.5?] +- [ ] добавить транзакции? [0.5?] +- [ ] **! добавить поддержку MySQL !** [0.5?] +- [ ] добавить метод для вызова процедур (SQL: CALL) `mysql` [0.5?] +- [ ] поддержка PosgreSQL [0.5?] +- [ ] **добавить исключения вместо сообщений об ошибках** [0.5?] +- [ ] добавить парсинг в объект? +- [ ] класс `Collection`, вынести туда методы `first()`, `last()`, `all()`, `one()` и т.п. +- [ ] класс `DBQuery`, вынести в него `query()` и т.п. возвращает класс `Collection`, добавить конвертацию в список (текущий формат) +- [ ] класс `DataMapper`, переводит таблицу в объект. поля таблицы = св-ва + +https://dev-gang.ru/article/cozdanie-s-nulja-prostoi-orm-na-python-5qz3j9ooul/ +https://www.sqlitetutorial.net/sqlite-subquery/ diff --git a/docs/Delete.md b/docs/Delete.md new file mode 100644 index 0000000..f411d22 --- /dev/null +++ b/docs/Delete.md @@ -0,0 +1,37 @@ +# DELETE +## Delete a row +```python +qb.delete('users')\ + .where([['name', '=', 'John']])\ + .go() +``` +or since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +qb.delete('users')\ + .where([['name', 'John']])\ + .go() +``` +Result query +```sql +DELETE FROM `users` WHERE `name` = 'John'; +``` +## Delete rows +```python +qb.delete('comments')\ + .where([['user_id', '=', 10]])\ + .go() +``` +or since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +qb.delete('comments')\ + .where([['user_id', 10]])\ + .go() +``` +Result query +```sql +DELETE FROM `comments` WHERE `user_id` = 10; +``` + +To the [TABLE section](Table.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Init.md b/docs/Init.md new file mode 100644 index 0000000..6291e33 --- /dev/null +++ b/docs/Init.md @@ -0,0 +1,21 @@ +# Initialization +## Import the module and init `QueryBuilder` with `Database()` +```python +from simple_query_builder import * + +# if you want to get results as a list of dictionaries (by default since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db') # result_dict=True, print_errors=False + +# or if you want to get results as a list of tuples (since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db', result_dict=False) + +# for printing errors into terminal (since 0.3.5) +qb = QueryBuilder(DataBase(), 'my_db.db', print_errors=True) + +# DB in memory +qb = QueryBuilder(DataBase(), ':memory:') +``` + +To the [Methods section](Methods.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Insert.md b/docs/Insert.md new file mode 100644 index 0000000..1062f4f --- /dev/null +++ b/docs/Insert.md @@ -0,0 +1,33 @@ +# INSERT +## Insert a row +```python +new_id = qb.insert('groups', { + 'name': 'Moderator', + 'permissions': 'moderator' +}).go() +``` +Result query +```sql +INSERT INTO `groups` (`name`, `permissions`) VALUES ('Moderator', 'moderator'); +``` +## Insert many rows +```python +qb.insert('groups', [['name', 'role'], + ['Moderator', 'moderator'], + ['Moderator2', 'moderator'], + ['User', 'user'], + ['User2', 'user'] +]).go() +``` +Result query +```sql +INSERT INTO `groups` (`name`, `role`) +VALUES ('Moderator', 'moderator'), + ('Moderator2', 'moderator'), + ('User', 'user'), + ('User2', 'user'); +``` + +To the [UPDATE section](Update.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Install.md b/docs/Install.md new file mode 100644 index 0000000..01767e5 --- /dev/null +++ b/docs/Install.md @@ -0,0 +1,16 @@ +# Installation + +Install the current version with [PyPI](https://pypi.org/project/simple-query-builder): + +```bash +pip install simple-query-builder +``` + +Or from Github: +```bash +pip install https://github.com/co0lc0der/simple-query-builder-python/archive/main.zip +``` + +To the [Init section](Init.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Methods.md b/docs/Methods.md new file mode 100644 index 0000000..816562d --- /dev/null +++ b/docs/Methods.md @@ -0,0 +1,26 @@ +# Main public methods +## QueryBuilder class +- `query(sql, params, fetch_type, col_index)` executes prepared `sql` with `params`, it can be used for custom queries +- `get_sql()` returns SQL query string which will be executed +- `__str__()` works the same as `get_sql()` +- `get_params()` returns a tuple of parameters for a query +- `get_result()` returns query's result +- `get_count()` returns result's rows count +- `has_error()` returns `True` if an error is had +- `get_error()` returns `True` if an error is had, ***this method will be changed in the next version!*** +- `get_error_message()` returns an error message if an error is had +- `set_error(message)` sets `_error` to `True` and `_error_message` +- `get_first()` returns the first item of results +- `get_last()` returns the last item of results +- `reset()` resets state to default values +- `all()` executes SQL query and returns **all rows** of result (`fetchall()`) +- `one()` executes SQL query and returns **the first row** of result (`fetchone()`) +- `column(col)` executes SQL query and returns the needed column of result by its index or name, `col` is `0` by default +- `pluck(key, col)` executes SQL query and returns a list of tuples/dicts (the key (usually ID) and the needed column of result) by its indexes or names, `key` is `0` and `col` is `1` by default +- `go()` this method is for non `SELECT` queries. it executes SQL query and returns nothing (but returns the last inserted row ID for `INSERT` method) +- `exists()` returns `True` if SQL query has at least one row and `False` if it hasn't +- `count()` prepares a query with SQL `COUNT(*)` function and _executes it_ + +'SQL' methods are presented in the [next section](Select.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Select.md b/docs/Select.md new file mode 100644 index 0000000..77524c3 --- /dev/null +++ b/docs/Select.md @@ -0,0 +1,186 @@ +# SELECT +## Simple queries +### Select all rows from a table +```python +results = qb.select('users').all() +``` +Result query +```sql +SELECT * FROM `users`; +``` +### Select a row with a condition +```python +results = qb.select('users').where([['id', '=', 10]]).one() +``` +It's able not using equals `=` in `WHERE` conditions since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +results = qb.select('users').where([['id', 10]]).one() +``` +Result query +```sql +SELECT * FROM `users` WHERE `id` = 10; +``` +### Select rows with two conditions +```python +results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', '=', 2]]).all() +``` +or since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +results = qb.select('users').where([['id', '>', 1], 'and', ['group_id', 2]]).all() +``` +Result query +```sql +SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2); +``` +### Select a row with a `LIKE` and `NOT LIKE` condition +```python +results = qb.select('users').like(['name', '%John%']).all() + +# or +results = qb.select('users').where([['name', 'LIKE', '%John%']]).all() +``` +or it's able to use two strings in parameters since [v0.3.5](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.5) +```python +results = qb.select('users').like('name', '%John%').all() +``` +Result query +```sql +SELECT * FROM `users` WHERE (`name` LIKE '%John%'); +``` +```python +results = qb.select('users').not_like(['name', '%John%']).all() + +# or +results = qb.select('users').where([['name', 'NOT LIKE', '%John%']]).all() +``` +or it's able to use two strings in parameters since [v0.3.5](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.5) +```python +results = qb.select('users').not_like('name', '%John%').all() +``` +Result query +```sql +SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%'); +``` +### Select a row with a `IS NULL` and `IS NOT NULL` condition +since [v0.3.5](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.5) +```python +results = qb.select('users').is_null('phone').all() + +# or +results = qb.select('users').where([['phone', 'is null']]).all() +``` +Result query +```sql +SELECT * FROM `users` WHERE (`phone` IS NULL); +``` +```python +results = qb.select('customers').is_not_null('address').all() + +# or +results = qb.select('customers').not_null('address').all() + +# or +results = qb.select('customers').where([['address', 'is not null']]).all() +``` +Result query +```sql +SELECT * FROM `customers` WHERE (`address` IS NOT NULL); +``` +### Select rows with `OFFSET` and `LIMIT` +```python +results = qb.select('posts')\ + .where([['user_id', '=', 3]])\ + .offset(14)\ + .limit(7)\ + .all() +``` +It's able not using equals `=` in `WHERE` conditions since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +results = qb.select('posts')\ + .where([['user_id', 3]])\ + .offset(14)\ + .limit(7)\ + .all() +``` +Result query +```sql +SELECT * FROM `posts` WHERE (`user_id` = 3) OFFSET 14 LIMIT 7; +``` +### Select custom fields with additional SQL +#### `COUNT()` +```python +results = qb.select('users', {'counter': 'COUNT(*)'}).one() + +# or +results = qb.count('users').one() +``` +Result query +```sql +SELECT COUNT(*) AS `counter` FROM `users`; +``` +#### `ORDER BY` +```python +results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ + .order_by('b.id', 'desc')\ + .all() +``` +It's able not using equals `=` in `WHERE` conditions since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +results = qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]])\ + .order_by('b.id desc')\ + .all() +``` +Result query +```sql +SELECT `b`.`id`, `b`.`name` FROM `branches` AS `b` +WHERE (`b`.`id` > 1) AND (`b`.`parent_id` = 1) +ORDER BY `b`.`id` DESC; +``` +#### `DISTINCT` +```python +results = qb.select('customers', ['city', 'country'], True).order_by('country desc').all() +``` +Result query +```sql +SELECT DISTINCT `city`, `country` FROM `customers` ORDER BY `country` DESC; +``` +#### `GROUP BY` and `HAVING` +```python +results = qb.select('posts', ['id', 'category', 'title'])\ + .where([['views', '>=', 1000]])\ + .group_by('category')\ + .all() +``` +Result query +```sql +SELECT `id`, `category`, `title` FROM `posts` +WHERE (`views` >= 1000) GROUP BY `category`; +``` +More complicated example +```python +groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ + .where([['YEAR(`created_at`)', '=', 2020]])\ + .group_by('month_num')\ + .having([['total', '=', 20000]])\ + .all() +``` +It's able not using equals `=` in `HAVING` conditions since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +groups = qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ + .where([['YEAR(`created_at`)', 2020]])\ + .group_by('month_num')\ + .having([['total', 20000]])\ + .all() +``` +Result query +```sql +SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` +FROM `orders` WHERE (YEAR(`created_at`) = 2020) +GROUP BY `month_num` HAVING (`total` = 20000); +``` + +To the [SELECT extensions section](Select_ext.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Select_ext.md b/docs/Select_ext.md new file mode 100644 index 0000000..f2e1a8d --- /dev/null +++ b/docs/Select_ext.md @@ -0,0 +1,186 @@ +# SELECT extensions +## `JOIN` +SQLite supports `INNER`, `LEFT OUTER` and `CROSS` joins (`INNER` is by default) +### `INNER JOIN` +```python +results = qb.select({'u': 'users'}, [ + 'u.id', + 'u.email', + 'u.username', + {'perms': 'groups.permissions'} + ])\ + .join('groups', ['u.group_id', 'groups.id'])\ + .limit(5)\ + .all() +``` +Result query +```sql +SELECT `u`.`id`, `u`.`email`, `u`.`username`, `groups`.`permissions` AS `perms` +FROM `users` AS `u` +INNER JOIN `groups` ON `u`.`group_id` = `groups`.`id` +LIMIT 5; +``` +More complicated examples +```python +results = qb.select({'cp': 'cabs_printers'}, [ + 'cp.id', + 'cp.cab_id', + {'cab_name': 'cb.name'}, + 'cp.printer_id', + {'printer_name': 'p.name'}, + {'cartridge_type': 'c.name'}, + 'cp.comment' + ])\ + .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ + .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ + .join({'c': 'cartridge_types'}, 'p.cartridge_id=c.id')\ + .where([['cp.cab_id', 'in', [11, 12, 13]], 'or', ['cp.cab_id', '=', 5], 'and', ['p.id', '>', 'c.id']])\ + .all() +``` +Result query +```sql +SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, + `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` +FROM `cabs_printers` AS `cp` +INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` +INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` +INNER JOIN `cartridge_types` AS `c` ON p.cartridge_id=c.id +WHERE (`cp`.`cab_id` IN (11, 12, 13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > `c`.`id`); +``` +It's able not using equals `=` in `JOIN` conditions since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +results = qb.select({'cp': 'cabs_printers'}, [ + 'cp.id', + 'cp.cab_id', + {'cab_name': 'cb.name'}, + 'cp.printer_id', + {'cartridge_id': 'c.id'}, + {'printer_name': 'p.name'}, + {'cartridge_type': 'c.name'}, + 'cp.comment' + ])\ + .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ + .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ + .join({'c': 'cartridge_types'}, ['p.cartridge_id', 'c.id'])\ + .group_by(['cp.printer_id', 'cartridge_id'])\ + .order_by(['cp.cab_id', 'cp.printer_id desc'])\ + .all() +``` +Result query +```sql +SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, `c`.`id` AS `cartridge_id`, + `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` +FROM `cabs_printers` AS `cp` +INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` +INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` +INNER JOIN `cartridge_types` AS `c` ON `p`.`cartridge_id` = `c`.`id` +GROUP BY `cp`.`printer_id`, `cartridge_id` +ORDER BY `cp`.`cab_id` ASC, `cp`.`printer_id` DESC; +``` +### `LEFT [OUTER] JOIN` +```python +# LEFT JOIN +results = qb.select('employees', ['employees.employee_id', 'employees.last_name', 'positions.title'])\ + .join('positions', ['employees.position_id', 'positions.position_id'], join_type="left")\ + .all() + +# or LEFT OUTER JOIN +results = qb.select('employees', ['employees.employee_id', 'employees.last_name', 'positions.title'])\ + .join('positions', ['employees.position_id', 'positions.position_id'], join_type="left outer")\ + .all() +``` +Result query +```sql +SELECT `employees`.`employee_id`, `employees`.`last_name`, `positions`.`title` FROM `employees` +LEFT [OUTER] JOIN `positions` ON `employees`.`position_id` = `positions`.`position_id`; +``` +## `INTERSECT` +Since [v0.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.4) +```python +results = qb.select('departments', ['department_id']).intersect_select('employees').all() +``` +Result query +```sql +SELECT `department_id` FROM `departments` +INTERSECT +SELECT `department_id` FROM `employees`; +``` +One more example +```python +results = qb.select('contacts', ['contact_id', 'last_name', 'first_name']).where([['contact_id', '>', 50]])\ + .intersect()\ + .select('customers', ['customer_id', 'last_name', 'first_name']).where([['last_name', '<>', 'Zagoskin']])\ + .all() +``` +Result query +```sql +SELECT `contact_id`, `last_name`, `first_name` FROM `contacts` WHERE (`contact_id` > 50) +INTERSECT +SELECT `customer_id`, `last_name`, `first_name` FROM `customers` WHERE (`last_name` <> 'Zagoskin'); +``` +## `EXCEPT` +Since [v0.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.4) +```python +results = qb.select('departments', ['department_id']).except_select('employees').all() +``` +Result query +```sql +SELECT `department_id` FROM `departments` +EXCEPT +SELECT `department_id` FROM `employees`; +``` +One more example +```python +results = qb.select('suppliers', ['supplier_id', 'state']).where([['state', 'Nevada']])\ + .excepts()\ + .select('companies', ['company_id', 'state']).where([['company_id', '<', 2000]])\ + .order_by('1 desc').all() +``` +Result query +```sql +SELECT `supplier_id`, `state` FROM `suppliers` WHERE (`state` = 'Nevada') +EXCEPT +SELECT `company_id`, `state` FROM `companies` WHERE (`company_id` < 2000) ORDER BY `1` DESC; +``` +## `UNION` and `UNION ALL` +Since [v0.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.4) +### `UNION` +```python +results = qb.select('clients', ['name', 'age'])\ + .union()\ + .select('employees', ['name', 'age'])\ + .all() + +# or +results = qb.select('clients', ['name', 'age'])\ + .union_select('employees')\ + .all() +``` +Result query +```sql +SELECT `name`, `age` FROM `clients` +UNION +SELECT `name`, `age` FROM `employees`; +``` +### `UNION ALL` +```python +results = qb.select('clients', ['name', 'age'])\ + .union(True)\ + .select('employees', ['name', 'age'])\ + .all() + +# or +results = qb.select('clients', ['name', 'age'])\ + .union_select('employees', True)\ + .all() +``` +Result query +```sql +SELECT `name`, `age` FROM `clients` +UNION ALL +SELECT `name`, `age` FROM `employees`; +``` + +To the [INSERT section](Insert.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Table.md b/docs/Table.md new file mode 100644 index 0000000..0284a51 --- /dev/null +++ b/docs/Table.md @@ -0,0 +1,35 @@ +# TABLE +## TRUNCATE +Truncate a table + +**! This method will be moved into _TableBuilder_ class !** +```python +qb.truncate('users').go() +``` +Result query +```sql +TRUNCATE TABLE `users`; +``` +## DROP +- Drop a table + +**! This method will be moved into _TableBuilder_ class !** +```python +qb.drop('temporary').go() +``` +Result query +```sql +DROP TABLE IF EXISTS `temporary`; +``` +- Without `IF EXISTS` +```python +qb.drop('temporary', False).go() +``` +Result query +```sql +DROP TABLE `temporary`; +``` + +To the [View section](View.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/Update.md b/docs/Update.md new file mode 100644 index 0000000..f97cc97 --- /dev/null +++ b/docs/Update.md @@ -0,0 +1,41 @@ +# UPDATE +## Update a row +```python +qb.update('users', { + 'username': 'John Doe', + 'status': 'new status' + })\ + .where([['id', '=', 7]])\ + .limit()\ + .go() +``` +or since [v0.3.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.3.4) +```python +qb.update('users', { + 'username': 'John Doe', + 'status': 'new status' + })\ + .where([['id', 7]])\ + .limit()\ + .go() +``` +Result query +```sql +UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' +WHERE `id` = 7 LIMIT 1; +``` +## Update rows +```python +qb.update('posts', {'status': 'published'})\ + .where([['YEAR(`updated_at`)', '>', 2020]])\ + .go() +``` +Result query +```sql +UPDATE `posts` SET `status` = 'published' +WHERE (YEAR(`updated_at`) > 2020); +``` + +To the [DELETE section](Delete.md) + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/View.md b/docs/View.md new file mode 100644 index 0000000..d81c6f1 --- /dev/null +++ b/docs/View.md @@ -0,0 +1,55 @@ +# VIEW +Since [v0.4](https://github.com/co0lc0der/simple-query-builder-python/releases/tag/v0.4) +## CREATE +Create a view from SELECT query +```python +qb.select('users')\ + .where([['email', 'is null'], 'or', ['email', '']])\ + .create_view('users_no_email')\ + .go() +``` +Result query +```sql +CREATE VIEW IF NOT EXISTS `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL) OR (`email` = ''); +``` +One more example +```python +qb.select('users')\ + .is_null('email')\ + .create_view('users_no_email')\ + .go() +``` +Result query +```sql +CREATE VIEW IF NOT EXISTS `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL); +``` +- Without `IF EXISTS` +```python +qb.select('users')\ + .is_null('email')\ + .create_view('users_no_email', False)\ + .go() +``` +Result query +```sql +CREATE VIEW `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL); +``` +## DROP +- Drop a view +```python +qb.drop_view('users_no_email').go() +``` +Result query +```sql +DROP VIEW IF EXISTS `users_no_email`; +``` +- Without `IF EXISTS` +```python +qb.drop_view('users_no_email', False).go() +``` +Result query +```sql +DROP VIEW `users_no_email`; +``` + +Back to [doc index](index.md) or [readme](../README.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7da2ea2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +# Index + +[![Latest Version](https://img.shields.io/github/release/co0lc0der/simple-query-builder-python?color=orange&style=flat-square)](https://github.com/co0lc0der/simple-query-builder-python/release) +![Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12](https://img.shields.io/pypi/pyversions/simple-query-builder?color=blueviolet&style=flat-square) + +- [Install](Install.md) +- [Initialization](Init.md) +- Main [public methods](Methods.md) +- Data manipulation + * [SELECT](Select.md) + + [SELECT extensions](Select_ext.md) (JOIN, UNION etc) + * [INSERT](Insert.md) + * [UPDATE](Update.md) + * [DELETE](Delete.md) +- Tables + * [TABLE](Table.md) +- Views + * [VIEW](View.md) + +Go to [readme](../README.md) From a03602f91bd6b92e1807e45343ae097809d0ea26 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 11:30:43 +0500 Subject: [PATCH 81/83] add SQL unittests --- tests/qb_delete_unittest.py | 34 ++++++ tests/qb_insert_unittest.py | 28 +++++ tests/qb_select_ext_unittest.py | 185 +++++++++++++++++++++++++++++ tests/qb_select_unittest.py | 203 ++++++++++++++++++++++++++++++++ tests/qb_update_unittest.py | 32 +++++ tests/qb_views_unittest.py | 39 ++++++ tests/tb_unittest.py | 27 +++++ 7 files changed, 548 insertions(+) create mode 100644 tests/qb_delete_unittest.py create mode 100644 tests/qb_insert_unittest.py create mode 100644 tests/qb_select_ext_unittest.py create mode 100644 tests/qb_select_unittest.py create mode 100644 tests/qb_update_unittest.py create mode 100644 tests/qb_views_unittest.py create mode 100644 tests/tb_unittest.py diff --git a/tests/qb_delete_unittest.py b/tests/qb_delete_unittest.py new file mode 100644 index 0000000..8796ab4 --- /dev/null +++ b/tests/qb_delete_unittest.py @@ -0,0 +1,34 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_delete_unittest.py -v + + +class QBDeleteTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_delete_eq(self): + sql = self.qb.delete('comments').where([['user_id', '=', 10]]).get_sql() + self.assertEqual(sql, "DELETE FROM `comments` WHERE (`user_id` = 10)") + self.assertEqual(self.qb.get_params(), (10, )) + + def test_sql_delete_no_eq(self): + sql = self.qb.delete('comments').where([['user_id', 10]]).get_sql() + self.assertEqual(sql, "DELETE FROM `comments` WHERE (`user_id` = 10)") + self.assertEqual(self.qb.get_params(), (10, )) + + # def test_sql_delete_limit_eq(self): + # sql = self.qb.delete('users').where([['name', '=', 'John']]).limit().get_sql() + # self.assertEqual(sql, "DELETE FROM `users` WHERE (`name` = 'John') LIMIT 1") + # self.assertEqual(self.qb.get_params(), ('John',)) + # + # def test_sql_delete_limit_no_eq(self): + # sql = self.qb.delete('users').where([['name', 'John']]).limit().get_sql() + # self.assertEqual(sql, "DELETE FROM `users` WHERE (`name` = 'John') LIMIT 1") + # self.assertEqual(self.qb.get_params(), ('John',)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qb_insert_unittest.py b/tests/qb_insert_unittest.py new file mode 100644 index 0000000..1c08d53 --- /dev/null +++ b/tests/qb_insert_unittest.py @@ -0,0 +1,28 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_insert_unittest.py -v + + +class QBInsertTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_insert(self): + sql = self.qb.insert('groups', {'name': 'Moderator', 'permissions': 'moderator'}).get_sql() + self.assertEqual(sql, "INSERT INTO `groups` (`name`, `permissions`) VALUES ('Moderator','moderator')") + self.assertEqual(self.qb.get_params(), ('Moderator', 'moderator')) + + def test_sql_insert_multiple(self): + sql = self.qb.insert('groups', [ + ['name', 'role'], + ['Moderator', 'moderator'], ['Moderator2', 'moderator'], + ['User', 'user'], ['User2', 'user'] + ]).get_sql() + self.assertEqual(sql, "INSERT INTO `groups` (`name`, `role`) VALUES ('Moderator','moderator'),('Moderator2','moderator'),('User','user'),('User2','user')") + self.assertEqual(self.qb.get_params(), ('Moderator', 'moderator', 'Moderator2', 'moderator', 'User', 'user', 'User2', 'user')) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qb_select_ext_unittest.py b/tests/qb_select_ext_unittest.py new file mode 100644 index 0000000..13ea47a --- /dev/null +++ b/tests/qb_select_ext_unittest.py @@ -0,0 +1,185 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_select_ext_unittest.py -v + + +class QBSelectExtensionsTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_select_inner_join_list(self): + sql = self.qb.select({'u': 'users'}, ['u.id', 'u.email', 'u.username', {'perms': 'groups.permissions'}])\ + .join('groups', ['u.group_id', 'groups.id']).get_sql() + self.assertEqual(sql, "SELECT `u`.`id`, `u`.`email`, `u`.`username`, `groups`.`permissions` AS `perms` FROM `users` AS `u` INNER JOIN `groups` ON `u`.`group_id` = `groups`.`id`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_inner_join3_str(self): + sql = self.qb.select({'cp': 'cabs_printers'}, [ + 'cp.id', 'cp.cab_id', {'cab_name': 'cb.name'}, 'cp.printer_id', + {'printer_name': 'p.name'}, {'cartridge_type': 'c.name'}, 'cp.comment' + ])\ + .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ + .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ + .join({'c': 'cartridge_types'}, 'p.cartridge_id=c.id')\ + .where([['cp.cab_id', 'in', [11, 12, 13]], 'or', ['cp.cab_id', 5], 'and', ['p.id', '>', 'c.id']]).get_sql() + self.assertEqual(sql, "SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` FROM `cabs_printers` AS `cp` INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` INNER JOIN `cartridge_types` AS `c` ON p.cartridge_id=c.id WHERE (`cp`.`cab_id` IN (11,12,13)) OR (`cp`.`cab_id` = 5) AND (`p`.`id` > 'c.id')") + self.assertEqual(self.qb.get_params(), (11, 12, 13, 5, 'c.id')) + + def test_sql_select_inner_join3_groupby_orederby(self): + sql = self.qb.select({'cp': 'cabs_printers'}, [ + 'cp.id', 'cp.cab_id', {'cab_name': 'cb.name'}, + 'cp.printer_id', {'cartridge_id': 'c.id'}, + {'printer_name': 'p.name'}, {'cartridge_type': 'c.name'}, 'cp.comment' + ])\ + .join({'cb': 'cabs'}, ['cp.cab_id', 'cb.id'])\ + .join({'p': 'printer_models'}, ['cp.printer_id', 'p.id'])\ + .join({'c': 'cartridge_types'}, ['p.cartridge_id', 'c.id'])\ + .group_by(['cp.printer_id', 'cartridge_id'])\ + .order_by(['cp.cab_id', 'cp.printer_id desc']).get_sql() + self.assertEqual(sql, "SELECT `cp`.`id`, `cp`.`cab_id`, `cb`.`name` AS `cab_name`, `cp`.`printer_id`, `c`.`id` AS `cartridge_id`, `p`.`name` AS `printer_name`, `c`.`name` AS `cartridge_type`, `cp`.`comment` FROM `cabs_printers` AS `cp` INNER JOIN `cabs` AS `cb` ON `cp`.`cab_id` = `cb`.`id` INNER JOIN `printer_models` AS `p` ON `cp`.`printer_id` = `p`.`id` INNER JOIN `cartridge_types` AS `c` ON `p`.`cartridge_id` = `c`.`id` GROUP BY `cp`.`printer_id`, `cartridge_id` ORDER BY `cp`.`cab_id` ASC, `cp`.`printer_id` DESC") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_left_join(self): + sql = self.qb.select('employees', ['employees.employee_id', 'employees.last_name', 'positions.title'])\ + .join('positions', ['employees.position_id', 'positions.position_id'], join_type="left").get_sql() + self.assertEqual(sql, "SELECT `employees`.`employee_id`, `employees`.`last_name`, `positions`.`title` FROM `employees` LEFT JOIN `positions` ON `employees`.`position_id` = `positions`.`position_id`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_left_outer_join(self): + sql = self.qb.select({'e': 'employees'}, ['e.employee_id', 'e.last_name', 'p.title'])\ + .join({'p': 'positions'}, ['e.position_id', 'p.position_id'], join_type="left outer").get_sql() + self.assertEqual(sql, "SELECT `e`.`employee_id`, `e`.`last_name`, `p`.`title` FROM `employees` AS `e` LEFT OUTER JOIN `positions` AS `p` ON `e`.`position_id` = `p`.`position_id`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_cross_join(self): + sql = self.qb.select('positions').join('departments', join_type="cross").get_sql() + self.assertEqual(sql, "SELECT * FROM `positions` CROSS JOIN `departments`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_except_select(self): + sql = self.qb.select('departments', ['department_id']).except_select('employees').get_sql() + self.assertEqual(sql, "SELECT `department_id` FROM `departments` EXCEPT SELECT `department_id` FROM `employees`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_excepts_where(self): + sql = self.qb.select('contacts', ['contact_id', 'last_name', 'first_name']).where([['contact_id', '>=', 74]])\ + .excepts()\ + .select('employees', ['employee_id', 'last_name', 'first_name']).where([['first_name', 'Sandra']])\ + .get_sql() + self.assertEqual(sql, "SELECT `contact_id`, `last_name`, `first_name` FROM `contacts` WHERE (`contact_id` >= 74) EXCEPT SELECT `employee_id`, `last_name`, `first_name` FROM `employees` WHERE (`first_name` = 'Sandra')") + self.assertEqual(self.qb.get_params(), (74, 'Sandra')) + + def test_sql_select_excepts_where_order_by(self): + sql = self.qb.select('suppliers', ['supplier_id', 'state']).where([['state', 'Nevada']]) \ + .excepts()\ + .select('companies', ['company_id', 'state']).where([['company_id', '<', 2000]]).order_by('1 desc')\ + .get_sql() + self.assertEqual(sql, "SELECT `supplier_id`, `state` FROM `suppliers` WHERE (`state` = 'Nevada') EXCEPT SELECT `company_id`, `state` FROM `companies` WHERE (`company_id` < 2000) ORDER BY `1` DESC") + self.assertEqual(self.qb.get_params(), ('Nevada', 2000)) + + def test_sql_intersect_select(self): + sql = self.qb.select('departments', ['department_id']).intersect_select('employees').get_sql() + self.assertEqual(sql, "SELECT `department_id` FROM `departments` INTERSECT SELECT `department_id` FROM `employees`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_intersect_where(self): + sql = self.qb.select('departments', 'department_id').where([['department_id', '>=', 25]])\ + .intersect()\ + .select('employees', 'department_id').where([['last_name', '<>', 'Petrov']])\ + .get_sql() + self.assertEqual(sql, "SELECT `department_id` FROM `departments` WHERE (`department_id` >= 25) INTERSECT SELECT `department_id` FROM `employees` WHERE (`last_name` <> 'Petrov')") + self.assertEqual(self.qb.get_params(), (25, 'Petrov')) + + def test_sql_select_intersect_where2(self): + sql = self.qb.select('contacts', ['contact_id', 'last_name', 'first_name']).where([['contact_id', '>', 50]])\ + .intersect()\ + .select('customers', ['customer_id', 'last_name', 'first_name']).where([['last_name', '<>', 'Zagoskin']])\ + .get_sql() + self.assertEqual(sql, "SELECT `contact_id`, `last_name`, `first_name` FROM `contacts` WHERE (`contact_id` > 50) INTERSECT SELECT `customer_id`, `last_name`, `first_name` FROM `customers` WHERE (`last_name` <> 'Zagoskin')") + self.assertEqual(self.qb.get_params(), (50, 'Zagoskin')) + + def test_sql_select_intersect_where_orderby(self): + sql = self.qb.select('departments', ['department_id', 'state']).where([['department_id', '>=', 25]]) \ + .intersect()\ + .select('companies', ['company_id', 'state']).like('company_name', 'G%').order_by('1')\ + .get_sql() + self.assertEqual(sql, "SELECT `department_id`, `state` FROM `departments` WHERE (`department_id` >= 25) INTERSECT SELECT `company_id`, `state` FROM `companies` WHERE (`company_name` LIKE 'G%') ORDER BY `1` ASC") + self.assertEqual(self.qb.get_params(), (25, 'G%')) + + def test_sql_select_union_where(self): + sql = self.qb.select('clients', ['name', 'age', {'total_sum': 'account_sum + account_sum * 0.1'}])\ + .where([['account_sum', '<', 3000]])\ + .union()\ + .select('clients', ['name', 'age', {'total_sum': 'account_sum + account_sum * 0.3'}])\ + .where([['account_sum', '>=', 3000]]).get_sql() + self.assertEqual(sql, "SELECT `name`, `age`, account_sum + account_sum * 0.1 AS `total_sum` FROM `clients` WHERE (`account_sum` < 3000) UNION SELECT `name`, `age`, account_sum + account_sum * 0.3 AS `total_sum` FROM `clients` WHERE (`account_sum` >= 3000)") + self.assertEqual(self.qb.get_params(), (3000, 3000)) + + def test_sql_union_select_where(self): + sql = self.qb.select('clients', ['name', 'age']).where([['id', '<', 10]])\ + .union_select('employees').where([['id', 1]]).get_sql() + self.assertEqual(sql, "SELECT `name`, `age` FROM `clients` WHERE (`id` < 10) UNION SELECT `name`, `age` FROM `employees` WHERE (`id` = 1)") + self.assertEqual(self.qb.get_params(), (10, 1)) + + def test_sql_select_union_where_orderby(self): + sql = self.qb.select('departments', ['department_id', 'department_name']).where([['department_id', 'in', [1, 2]]])\ + .union()\ + .select('employees', ['employee_id', 'last_name']).where([['hire_date', '2024-02-08']]).order_by('2')\ + .get_sql() + self.assertEqual(sql, "SELECT `department_id`, `department_name` FROM `departments` WHERE (`department_id` IN (1,2)) UNION SELECT `employee_id`, `last_name` FROM `employees` WHERE (`hire_date` = '2024-02-08') ORDER BY `2` ASC") + self.assertEqual(self.qb.get_params(), (1, 2, '2024-02-08')) + + def test_sql_select_union_all(self): + sql = self.qb.select('clients', ['name', 'age', {'total_sum': 'account_sum + account_sum * 0.1'}])\ + .where([['account_sum', '<', 3000]])\ + .union(True)\ + .select('clients', ['name', 'age', {'total_sum': 'account_sum + account_sum * 0.3'}])\ + .where([['account_sum', '>=', 3000]]).get_sql() + self.assertEqual(sql, "SELECT `name`, `age`, account_sum + account_sum * 0.1 AS `total_sum` FROM `clients` WHERE (`account_sum` < 3000) UNION ALL SELECT `name`, `age`, account_sum + account_sum * 0.3 AS `total_sum` FROM `clients` WHERE (`account_sum` >= 3000)") + self.assertEqual(self.qb.get_params(), (3000, 3000)) + + def test_sql_union_select_all_where(self): + sql = self.qb.select('cabs', ['id', 'name']) \ + .union_select('printer_models', True).where([['id', '<', 10]]) \ + .get_sql() + self.assertEqual(sql, "SELECT `id`, `name` FROM `cabs` UNION ALL SELECT `id`, `name` FROM `printer_models` WHERE (`id` < 10)") + self.assertEqual(self.qb.get_params(), (10, )) + + def test_sql_select_union_all_where_orderby(self): + sql = self.qb.select('departments', ['department_id', 'department_name']).where([['department_id', '>=', 10]])\ + .union(True)\ + .select('employees', ['employee_id', 'last_name']).where([['last_name', 'Rassohin']]).order_by('2')\ + .get_sql() + self.assertEqual(sql, "SELECT `department_id`, `department_name` FROM `departments` WHERE (`department_id` >= 10) UNION ALL SELECT `employee_id`, `last_name` FROM `employees` WHERE (`last_name` = 'Rassohin') ORDER BY `2` ASC") + self.assertEqual(self.qb.get_params(), (10, 'Rassohin')) + + # def test_sql_select_in_select_str_one_param(self): + # sql = self.qb.select(self.qb.select('categories').get_sql(), ['id', 'name']).where([['id', '<=', 5]]).get_sql() + # self.assertEqual(sql, "SELECT `id`, `name` FROM (SELECT * FROM `categories`) WHERE (`id` <= 5)") + # self.assertEqual(self.qb.get_params(), (5, )) + # + # def test_sql_select_in_select_str_two_params(self): + # sql = self.qb.select(self.qb.select('categories').where([['parent_id', 0]]).get_sql(), ['id', 'name']).where([['id', '<=', 5]]).get_sql() + # self.assertEqual(sql, "SELECT `id`, `name` FROM (SELECT * FROM `categories` WHERE (`parent_id` = 0)) WHERE (`id` <= 5)") + # self.assertEqual(self.qb.get_params(), (0, 5)) + # + # def test_sql_select_in_select_add_query(self): + # q1 = self.qb.select('categories').where([['parent_id', 0]]).get_sql() + # sql = self.qb.select(q1, ['id', 'name']).where([['id', '<=', 5]]).get_sql() + # self.assertEqual(sql, "SELECT `id`, `name` FROM (SELECT * FROM `categories` WHERE (`parent_id` = 0)) WHERE (`id` <= 5)") + # self.assertEqual(self.qb.get_params(), (0, 5)) + # + # def test_sql_select_in_select_add_query_format(self): + # q1 = self.qb.select('categories').where([['parent_id', 0]]) + # sql = self.qb.select(f'{q1}', ['id', 'name']).where([['id', '<=', 5]]).get_sql() + # self.assertEqual(sql, "SELECT `id`, `name` FROM (SELECT * FROM `categories` WHERE (`parent_id` = 0)) WHERE (`id` <= 5)") + # self.assertEqual(self.qb.get_params(), (0, 5)) + + def tearDown(self): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qb_select_unittest.py b/tests/qb_select_unittest.py new file mode 100644 index 0000000..145db22 --- /dev/null +++ b/tests/qb_select_unittest.py @@ -0,0 +1,203 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_select_unittest.py -v + + +class QBSelectTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_select_all(self): + sql = self.qb.select('users').get_sql() + self.assertEqual(sql, 'SELECT * FROM `users`') + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_where_eq(self): + sql = self.qb.select('users').where([['id', '=', 10]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` = 10)') + self.assertEqual(self.qb.get_params(), (10,)) + + def test_sql_select_where_no_eq(self): + sql = self.qb.select('users').where([['id', 10]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` = 10)') + self.assertEqual(self.qb.get_params(), (10, )) + + def test_sql_select_where_and_eq(self): + sql = self.qb.select('users').where([['id', '>', 1], 'and', ['group_id', '=', 2]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2)') + self.assertEqual(self.qb.get_params(), (1, 2)) + + def test_sql_select_where_and_no_eq(self): + sql = self.qb.select('users').where([['id', '>', 1], 'and', ['group_id', 2]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` > 1) AND (`group_id` = 2)') + self.assertEqual(self.qb.get_params(), (1, 2)) + + def test_sql_select_where_or_eq(self): + sql = self.qb.select('users').where([['id', '>', 1], 'or', ['group_id', '=', 2]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` > 1) OR (`group_id` = 2)') + self.assertEqual(self.qb.get_params(), (1, 2)) + + def test_sql_select_where_or_no_eq(self): + sql = self.qb.select('users').where([['id', '>', 1], 'or', ['group_id', 2]]).get_sql() + self.assertEqual(sql, 'SELECT * FROM `users` WHERE (`id` > 1) OR (`group_id` = 2)') + self.assertEqual(self.qb.get_params(), (1, 2)) + + def test_sql_select_where_like(self): + sql = self.qb.select('users').where([['name', 'LIKE', '%John%']]).get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_like_list(self): + sql = self.qb.select('users').like(['name', '%John%']).get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_like_str(self): + sql = self.qb.select('users').like('name', '%John%').get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_where_not_like(self): + sql = self.qb.select('users').where([['name', 'NOT LIKE', '%John%']]).get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_not_like_list(self): + sql = self.qb.select('users').not_like(['name', '%John%']).get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_not_like_str(self): + sql = self.qb.select('users').not_like('name', '%John%').get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`name` NOT LIKE '%John%')") + self.assertEqual(self.qb.get_params(), ('%John%',)) + + def test_sql_select_where_is_null(self): + sql = self.qb.select('users').where([['phone', 'is null']]).get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`phone` IS NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_is_null(self): + sql = self.qb.select('users').is_null('phone').get_sql() + self.assertEqual(sql, "SELECT * FROM `users` WHERE (`phone` IS NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_where_is_not_null(self): + sql = self.qb.select('customers').where([['address', 'is not null']]).get_sql() + self.assertEqual(sql, "SELECT * FROM `customers` WHERE (`address` IS NOT NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_not_null(self): + sql = self.qb.select('customers').not_null('address').get_sql() + self.assertEqual(sql, "SELECT * FROM `customers` WHERE (`address` IS NOT NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_is_not_null(self): + sql = self.qb.select('customers').is_not_null('address').get_sql() + self.assertEqual(sql, "SELECT * FROM `customers` WHERE (`address` IS NOT NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_offset(self): + sql = self.qb.select('posts').where([['user_id', 3]]).offset(14).get_sql() + self.assertEqual(sql, "SELECT * FROM `posts` WHERE (`user_id` = 3) OFFSET 14") + self.assertEqual(self.qb.get_params(), (3,)) + + def test_sql_select_limit(self): + sql = self.qb.select('posts').where([['id', '>', 42]]).limit(7).get_sql() + self.assertEqual(sql, "SELECT * FROM `posts` WHERE (`id` > 42) LIMIT 7") + self.assertEqual(self.qb.get_params(), (42, )) + + def test_sql_select_counter(self): + sql = self.qb.select('users', {'counter': 'COUNT(*)'}).get_sql() + self.assertEqual(sql, "SELECT COUNT(*) AS `counter` FROM `users`") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_distinct_order_by(self): + sql = self.qb.select('customers', ['city'], True).order_by('city').get_sql() + self.assertEqual(sql, "SELECT DISTINCT `city` FROM `customers` ORDER BY `city` ASC") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_distinct_order_by_2col(self): + sql = self.qb.select('customers', ['city', 'country'], True).order_by('country desc').get_sql() + self.assertEqual(sql, "SELECT DISTINCT `city`, `country` FROM `customers` ORDER BY `country` DESC") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_order_by_two_param(self): + sql = self.qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]]).order_by('b.id', 'desc').get_sql() + self.assertEqual(sql, "SELECT `b`.`id`, `b`.`name` FROM `branches` AS `b` WHERE (`b`.`id` > 1) AND (`b`.`parent_id` = 1) ORDER BY `b`.`id` DESC") + self.assertEqual(self.qb.get_params(), (1, 1)) + + def test_sql_select_order_by_one_param(self): + sql = self.qb.select({'b': 'branches'}, ['b.id', 'b.name'])\ + .where([['b.id', '>', 1], 'and', ['b.parent_id', 1]]).order_by('b.id desc').get_sql() + self.assertEqual(sql, "SELECT `b`.`id`, `b`.`name` FROM `branches` AS `b` WHERE (`b`.`id` > 1) AND (`b`.`parent_id` = 1) ORDER BY `b`.`id` DESC") + self.assertEqual(self.qb.get_params(), (1, 1)) + + def test_sql_select_group_by(self): + sql = self.qb.select('posts', ['id', 'category', 'title'])\ + .where([['views', '>=', 1000]]).group_by('category').get_sql() + self.assertEqual(sql, "SELECT `id`, `category`, `title` FROM `posts` WHERE (`views` >= 1000) GROUP BY `category`") + self.assertEqual(self.qb.get_params(), (1000, )) + + def test_sql_select_group_by_having_eq(self): + sql = self.qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ + .where([['YEAR(`created_at`)', 2020]]).group_by('month_num')\ + .having([['total', '=', 20000]]).get_sql() + self.assertEqual(sql, "SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` FROM `orders` WHERE (YEAR(`created_at`) = 2020) GROUP BY `month_num` HAVING (`total` = 20000)") + self.assertEqual(self.qb.get_params(), (2020, 20000)) + + def test_sql_select_group_by_having_no_eq_sum(self): + sql = self.qb.select('orders', {'month_num': 'MONTH(`created_at`)', 'total': 'SUM(`total`)'})\ + .where([['YEAR(`created_at`)', 2020]]).group_by('month_num')\ + .having([['total', 20000]]).get_sql() + self.assertEqual(sql, "SELECT MONTH(`created_at`) AS `month_num`, SUM(`total`) AS `total` FROM `orders` WHERE (YEAR(`created_at`) = 2020) GROUP BY `month_num` HAVING (`total` = 20000)") + self.assertEqual(self.qb.get_params(), (2020, 20000)) + + def test_sql_select_group_by_having_max(self): + sql = self.qb.select('employees', ['department', {'Highest salary': 'MAX(`salary`)'}])\ + .where([['favorite_website', 'Google.com']]).group_by('department')\ + .having([['MAX(`salary`)', '>=', 30000]]).get_sql() + self.assertEqual(sql, "SELECT `department`, MAX(`salary`) AS `Highest salary` FROM `employees` WHERE (`favorite_website` = 'Google.com') GROUP BY `department` HAVING (MAX(`salary`) >= 30000)") + self.assertEqual(self.qb.get_params(), ('Google.com', 30000)) + + def test_sql_select_group_by_having_count(self): + sql = self.qb.select('employees', ['department', {'Number of employees': 'COUNT(*)'}])\ + .where([['state', 'Nevada']]).group_by('department')\ + .having([['COUNT(*)', '>', 20]]).get_sql() + self.assertEqual(sql, "SELECT `department`, COUNT(*) AS `Number of employees` FROM `employees` WHERE (`state` = 'Nevada') GROUP BY `department` HAVING (COUNT(*) > 20)") + self.assertEqual(self.qb.get_params(), ('Nevada', 20)) + + def test_sql_select_summ(self): + sql = self.qb.select("1+5 as 'res'").get_sql() + self.assertEqual(sql, "SELECT 1+5 as 'res'") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_sub(self): + sql = self.qb.select("10 - 3 as 'res'").get_sql() + self.assertEqual(sql, "SELECT 10 - 3 as 'res'") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_substr(self): + sql = self.qb.select("substr('Hello world!', 1, 5) as 'str'").get_sql() + self.assertEqual(sql, "SELECT substr('Hello world!', 1, 5) as 'str'") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_sqlite_version(self): + sql = self.qb.select("sqlite_version() as ver").get_sql() + self.assertEqual(sql, "SELECT sqlite_version() as ver") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_select_time(self): + sql = self.qb.select("strftime('%Y-%m-%d %H:%M', 'now')").get_sql() + self.assertEqual(sql, "SELECT strftime('%Y-%m-%d %H:%M', 'now')") + self.assertEqual(self.qb.get_params(), ()) + + def tearDown(self): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qb_update_unittest.py b/tests/qb_update_unittest.py new file mode 100644 index 0000000..cb33602 --- /dev/null +++ b/tests/qb_update_unittest.py @@ -0,0 +1,32 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_update_unittest.py -v + + +class QBUpdateTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_update(self): + sql = self.qb.update('posts', {'status': 'published'})\ + .where([['YEAR(`updated_at`)', '>', 2020]]).get_sql() + self.assertEqual(sql, "UPDATE `posts` SET `status` = 'published' WHERE (YEAR(`updated_at`) > 2020)") + self.assertEqual(self.qb.get_params(), ('published', 2020)) + + def test_sql_update_limit_eq(self): + sql = self.qb.update('users', {'username': 'John Doe', 'status': 'new status'})\ + .where([['id', '=', 7]]).limit().get_sql() + self.assertEqual(sql, "UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' WHERE (`id` = 7) LIMIT 1") + self.assertEqual(self.qb.get_params(), ('John Doe', 'new status', 7)) + + def test_sql_update_limit_no_eq(self): + sql = self.qb.update('users', {'username': 'John Doe', 'status': 'new status'})\ + .where([['id', 7]]).limit().get_sql() + self.assertEqual(sql, "UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' WHERE (`id` = 7) LIMIT 1") + self.assertEqual(self.qb.get_params(), ('John Doe', 'new status', 7)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qb_views_unittest.py b/tests/qb_views_unittest.py new file mode 100644 index 0000000..7bcb1ba --- /dev/null +++ b/tests/qb_views_unittest.py @@ -0,0 +1,39 @@ +import unittest +from querybuilder import * + +# test run +# python .\qb_views_unittest.py -v + + +class QBViewsTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_create_view_exists(self): + sql = self.qb.select('users').is_null('email').create_view('users_no_email').get_sql() + self.assertEqual(sql, "CREATE VIEW IF NOT EXISTS `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_create_view_no_exists(self): + sql = self.qb.select('users').is_null('email').create_view('users_no_email', False).get_sql() + self.assertEqual(sql, "CREATE VIEW `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL)") + self.assertEqual(self.qb.get_params(), ()) + + def test_sql_create_view(self): + sql = self.qb.select('users').where([['email', 'is null'], 'or', ['email', '']])\ + .create_view('users_no_email').get_sql() + self.assertEqual(sql, "CREATE VIEW IF NOT EXISTS `users_no_email` AS SELECT * FROM `users` WHERE (`email` IS NULL) OR (`email` = '')") + self.assertEqual(self.qb.get_params(), ('', )) + # self.qb.go() + + def test_sql_drop_view_no_exists(self): + sql = self.qb.drop_view('users_no_email', False).get_sql() + self.assertEqual(sql, "DROP VIEW `users_no_email`") + + def test_sql_drop_view_exists(self): + sql = self.qb.drop_view('users_no_email').get_sql() + self.assertEqual(sql, "DROP VIEW IF EXISTS `users_no_email`") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tb_unittest.py b/tests/tb_unittest.py new file mode 100644 index 0000000..6fcd298 --- /dev/null +++ b/tests/tb_unittest.py @@ -0,0 +1,27 @@ +import unittest +from simple_query_builder.querybuilder import * + +# test run +# python .\tb_unittest.py -v +# python -m unittest .\tb_unittest.py + + +class TBTestCase(unittest.TestCase): + def setUp(self): + self.qb = QueryBuilder(DataBase(), ":memory:") + + def test_sql_drop_table_no_exists(self): + sql = self.qb.drop('temporary', False).get_sql() + self.assertEqual(sql, "DROP TABLE `temporary`") + + def test_sql_drop_table_exists(self): + sql = self.qb.drop('temporary').get_sql() + self.assertEqual(sql, "DROP TABLE IF EXISTS `temporary`") + + def test_sql_truncate_table(self): + sql = self.qb.truncate('users').get_sql() + self.assertEqual(sql, "TRUNCATE TABLE `users`") + + +if __name__ == "__main__": + unittest.main() From 7418c5f242481027fc490432f583f69a1f8d8743 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 14:04:20 +0500 Subject: [PATCH 82/83] update package version --- README.md | 2 +- setup.cfg | 2 +- setup.py | 10 ++++++---- simple_query_builder/__init__.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c9c9eb4..cf246a4 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Result query UPDATE `users` SET `username` = 'John Doe', `status` = 'new status' WHERE `id` = 7 LIMIT 1; ``` -More examples you can find in [documentation](docs/index.md) +More examples you can find in [documentation](https://github.com/co0lc0der/simple-query-builder-python/blob/main/docs/index.md) ## ToDo I'm going to add the next features into future versions diff --git a/setup.cfg b/setup.cfg index a7468af..36ff2f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = 0.3.6 +tag_build = 0.4 tag_date = 0 diff --git a/setup.py b/setup.py index e1251c1..8218d85 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,10 @@ """ :authors: co0lc0der :license: MIT -:copyright: (c) 2022-2023 co0lc0der +:copyright: (c) 2022-2024 co0lc0der """ -version = '0.3.6' +version = '0.4' with open('README.md', encoding='utf-8') as f: long_description = f.read() @@ -43,7 +43,6 @@ 'Topic :: Database :: Database Engines/Servers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 3', @@ -51,8 +50,11 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: SQL' + 'Programming Language :: SQL', + 'Typing :: Typed' ] ) diff --git a/simple_query_builder/__init__.py b/simple_query_builder/__init__.py index 5d061eb..6fb9ec1 100644 --- a/simple_query_builder/__init__.py +++ b/simple_query_builder/__init__.py @@ -7,5 +7,5 @@ from .querybuilder import * __author__ = 'co0lc0der' -__version__ = '0.3.6' +__version__ = '0.4' __email__ = 'c0der@ya.ru' From 3111c83cb7e1387c197d3423e3c07afdd0daa9c1 Mon Sep 17 00:00:00 2001 From: co0lc0der Date: Wed, 25 Dec 2024 15:20:20 +0500 Subject: [PATCH 83/83] update ToDo --- ToDo.md | 58 +++++++++++---------------------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/ToDo.md b/ToDo.md index 2e79edb..fa4dfa5 100644 --- a/ToDo.md +++ b/ToDo.md @@ -1,47 +1,11 @@ -# TODO -- [x] `order_by()`: добавить возможность указывать несколько полей для сортировки [0.3.4] -- [x] `group_by()`: добавить возможность указывать несколько полей [0.3.4] -- [x] `_prepare_conditions()`: если 2й параметр (вместо условия) не оператор, то оператор `=` [0.3.4] -- [x] `_prepare_conditions()`: если 2й параметр список, то оператор `IN` [0.3.4] -- [x] добавить метод `pluck(id, column)` (как в Laravel) [0.3.4] -- [x] добавить методы `is_null(field)`, `is_not_null(field)` и `not_null(field)` [0.3.5] -- [x] в методах `like()` и `not_like()` можно без `[]` [0.3.5] -- [x] св-во `_print_errors` и параметр в `__init__()` [0.3.5] -- [x] параметр `result_dict` в `QB.__init__()` [0.3.5] -- [x] в методе `get_sql()` `?` меняются на соответствующие значения [0.3.5] -- [x] рефактор методов `query()`, `pluck()` и `column()` [0.3.6] -- [X] вынести в отдельный файл классы `MetaSingleton` и `DataBase` [0.4] -- [X] добавить мат.операторы (и не только) в поля [0.4] -- [X] добавить `UNION` и `UNION ALL` [0.4] -- [X] добавить `DISTINCT` в `SELECT` [0.4] -- [X] добавить `EXCEPT` в `SELECT` [0.4] -- [X] добавить `INTERSECT` в `SELECT` [0.4] -- [X] в методе `get_sql()` параметр `with_values=True` [0.4] -- [X] добавить метод `has_error()` на замену `get_error()` в след.версии [0.4] -- [X] добавить метод `__str__()` [0.4] -- [X] добавить `list` в типы параметров `table` [0.4] -- [X] покрыть тестами SQL методы: `SELECT, INSERT, UPDATE, DELETE` (примеры из README + новые методы) [0.4] -- [X] разделить документацию на файлы [0.4] -- [X] добавить новые фун-ии в документацию [0.4] -- [X] добавить `CREATE VIEW view_name AS SQL_SELECT` [0.4] -- [X] добавить `DROP VIEW view_name` [0.4] -- [ ] в `.where()` можно передавать по 1 или 2 строки вместо списка [0.4?] -- [ ] добавить скобки `()` в выражения (subquery). сложенные выражения (select в select'e), использование в field, from, where, join [0.5?] -- [ ] добавить `WHERE EXISTS` [0.5?] -- [ ] добавить `BETWEEN` [0.5?] -- [ ] добавить `WITH` [0.5?] -- [ ] создать **класс `TableBuilder`** для манипуляций с таблицами [0.5?] -- [ ] перенести в **`TableBuilder`** методы `qb.drop()` и `qb.truncate()` [0.5?] -- [ ] перенести в **`TableBuilder`** методы `qb.create_view()` и `qb.drop_view()` [0.5?] -- [ ] добавить транзакции? [0.5?] -- [ ] **! добавить поддержку MySQL !** [0.5?] -- [ ] добавить метод для вызова процедур (SQL: CALL) `mysql` [0.5?] -- [ ] поддержка PosgreSQL [0.5?] -- [ ] **добавить исключения вместо сообщений об ошибках** [0.5?] -- [ ] добавить парсинг в объект? -- [ ] класс `Collection`, вынести туда методы `first()`, `last()`, `all()`, `one()` и т.п. -- [ ] класс `DBQuery`, вынести в него `query()` и т.п. возвращает класс `Collection`, добавить конвертацию в список (текущий формат) -- [ ] класс `DataMapper`, переводит таблицу в объект. поля таблицы = св-ва - -https://dev-gang.ru/article/cozdanie-s-nulja-prostoi-orm-na-python-5qz3j9ooul/ -https://www.sqlitetutorial.net/sqlite-subquery/ +## ToDo +I'm going to add the next features into future versions +- write more unit testes +- add subqueries for QueryBuilder +- add `BETWEEN` +- add `WHERE EXISTS` +- add TableBuilder class (for beginning `CREATE TABLE`, move `qb.drop()` and `qb.truncate()` into it) +- add MySQL support +- add PostgreSQL support +- add `WITH` +- and probably something more