├── setup.cfg
├── MAUNIFEST.in
├── tests
├── test.sql
├── dbt.py
└── pymysqlt.py
├── CHANGES
├── .gitattributes
├── db
├── mysql
│ ├── __init__.py
│ ├── dialect.py
│ └── connection.py
├── pymysql
│ ├── __init__.py
│ ├── dialect.py
│ └── connection.py
├── query
│ ├── __init__.py
│ ├── expr.py
│ ├── base.py
│ ├── update.py
│ ├── delete.py
│ ├── insert.py
│ └── select.py
├── errors.py
├── connection.py
├── pool.py
├── dialect.py
├── __init__.py
└── _db.py
├── samples
├── schema.sql
├── app.py
├── model.py
└── orm.py
├── setup.py
├── .gitignore
├── LICENSE
├── README_CN.rst
└── README.rst
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal=1
--------------------------------------------------------------------------------
/MAUNIFEST.in:
--------------------------------------------------------------------------------
1 | include CHANGES
2 | include LICENSE
3 | include MANIFEST.in
4 | include README.rst
5 | recursive-include tests *
--------------------------------------------------------------------------------
/tests/test.sql:
--------------------------------------------------------------------------------
1 |
2 | CREATE TABLE `users` (
3 | `uid` int(11) unsigned NOT NULL AUTO_INCREMENT,
4 | `name` varchar(60) DEFAULT NULL,
5 | PRIMARY KEY (`uid`)
6 | );
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | dbpy
2 | =============
3 |
4 | Version 0.1
5 | -----------
6 |
7 | Released on March 09 2015
8 |
9 |
10 | - silmple and flexible
11 | - graceful and useful sql query builder.
12 | - thread-safe connection pool
13 | - supports read/write master-slave mode
14 | - supports transaction
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/db/mysql/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | __class_prefix__ = 'MySQL'
--------------------------------------------------------------------------------
/db/pymysql/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | __class_prefix__ = 'PyMySQL'
--------------------------------------------------------------------------------
/db/query/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 |
18 | LOGGER = logging.getLogger('db.query')
--------------------------------------------------------------------------------
/db/errors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | class DBError(Exception): pass
17 |
18 | class NotInstallDriverError(DBError): pass
19 |
20 | class ConnectError(DBError): pass
21 |
--------------------------------------------------------------------------------
/db/mysql/dialect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.dialect import Dialect
17 |
18 | class MySQLDialect(Dialect):
19 |
20 | def initialize(self):
21 | self._identifier = '`'
--------------------------------------------------------------------------------
/db/pymysql/dialect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.dialect import Dialect
17 |
18 | class PyMySQLDialect(Dialect):
19 |
20 | def initialize(self):
21 | self._identifier = '`'
--------------------------------------------------------------------------------
/samples/schema.sql:
--------------------------------------------------------------------------------
1 |
2 | DROP TABLE IF EXISTS `users`;
3 | CREATE TABLE `users` (
4 | `uid` MEDIUMINT(8) unsigned NOT NULL AUTO_INCREMENT,
5 | `username` varchar(140) NOT NULL,
6 | `real_name` varchar(140) NOT NULL,
7 | `email` VARCHAR(140) NOT NULL,
8 | `password` CHAR(56) NOT NULL,
9 | `bio` text NOT NULL,
10 | `status` enum('inactive','active') NOT NULL,
11 | `role` enum('administrator','editor','user') NOT NULL,
12 |
13 | PRIMARY KEY (`uid`),
14 | UNIQUE KEY (`email`)
15 |
16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
17 |
18 |
19 | DROP TABLE IF EXISTS `posts`;
20 | CREATE TABLE `posts` (
21 | `pid` int(6) NOT NULL AUTO_INCREMENT,
22 | `title` varchar(150) NOT NULL,
23 | `slug` varchar(150) NOT NULL,
24 | `description` text NOT NULL,
25 | `html` text NOT NULL,
26 | `css` text NOT NULL,
27 | `js` text NOT NULL,
28 | `created` datetime NOT NULL,
29 | `author` int(6) NOT NULL,
30 | `category` int(6) NOT NULL,
31 | `status` enum('draft','published','archived') NOT NULL,
32 | `comments` tinyint(1) NOT NULL,
33 |
34 | PRIMARY KEY (`pid`),
35 | KEY `status` (`status`),
36 | KEY `slug` (`slug`)
37 | ) ENGINE=InnoDB CHARSET=utf8;
--------------------------------------------------------------------------------
/db/query/expr.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | class Expr(object):
18 | """Sql expresion builder"""
19 |
20 | def __init__(self, expression, alias=None):
21 | #: sql expresion
22 | self.expression = expression
23 | #: expresssion filed name
24 | self.alias = alias
25 |
26 | def compile(self, db):
27 | """Building the sql expression
28 |
29 | :param db: the database instance
30 | """
31 | sql = self.expression
32 | if self.alias:
33 | sql += (' AS ' + db.quote_column(self.alias))
34 | return sql
35 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup, find_packages
4 | import sys
5 | from db import __version__
6 | from codecs import open
7 |
8 | with open('README.rst', 'r', 'utf-8') as f:
9 | readme = f.read()
10 |
11 | setup(
12 | name='dbpy',
13 | version=__version__,
14 | author="Thomas Huang",
15 | author_email='lyanghwy@gmail.com',
16 | description="database abstraction layer for pythoneer",
17 | long_description=readme,
18 | license="GPL",
19 | keywords="database abstraction layer for pythoneer(orm, database)",
20 | url='https://github.com/whiteclover/dbpy',
21 | packages=find_packages(exclude=['samples', 'tests*']),
22 | zip_safe=False,
23 | include_package_data=True,
24 | install_requires=['setuptools'],
25 | test_suite='unittest',
26 | classifiers=[
27 | 'Development Status :: 3 - Alpha',
28 | 'License :: OSI Approved :: GNU Affero General Public License v3',
29 | 'Natural Language :: English',
30 | 'Programming Language :: Python :: 3',
31 | 'Programming Language :: Python :: 3.4',
32 | 'Programming Language :: Python :: 3.5',
33 | 'Programming Language :: Python :: 3.6',
34 | 'Programming Language :: Python :: Implementation :: CPython',
35 | 'Programming Language :: Python :: Implementation :: PyPy',
36 | 'Operating System :: OS Independent',
37 | 'Topic :: Database'
38 | ]
39 | )
40 |
--------------------------------------------------------------------------------
/db/query/base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | class Query(object):
18 | """Base Sql Query class"""
19 |
20 | def __init__(self, dialect):
21 | """The sql dialect for building sql"""
22 | self.dialect = dialect
23 |
24 | def close(self):
25 | """Reset the dialect to none"""
26 | self.dialect = None
27 |
28 | def __del__(self):
29 | self.close()
30 |
31 | def to_sql(self):
32 | """Generates the sql statement"""
33 | return self.compile()
34 |
35 | def compile(self):
36 | """Comile the sql statement"""
37 | raise NotImplementedError("Implements ``compile`` in subclass...")
38 |
39 | __str__ = to_sql
40 |
41 | def execute(self):
42 | """excute database sql operator"""
43 | raise NotImplementedError("Implements ``execute`` in subclass...")
44 |
--------------------------------------------------------------------------------
/samples/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | import model
18 | from orm import Backend
19 | import db
20 |
21 | db.setup({'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'})
22 |
23 |
24 | user = Backend('user').find_by_username('username')
25 | if user and user.check('password'):
26 | print('auth')
27 |
28 | user = model.User('username', 'email', 'real_name', 'password', 'bio', 'status', 'role')
29 | if Backend('user').create(user):
30 | print('fine')
31 |
32 | user = Backend('user').find(12)
33 | user.real_name = 'blablabla....'
34 | if Backend('user').save(user):
35 | print('user saved')
36 |
37 | if Backend('user').delete(user):
38 | print('delete user failed')
39 |
40 |
41 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js', 'category', 'status', 'comments', 'author')
42 | if not Backend('post').create(post):
43 | print('created failed')
44 |
--------------------------------------------------------------------------------
/db/query/update.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.query.select import WhereQuery
17 |
18 |
19 | class UpdateQuery(WhereQuery):
20 |
21 | def __init__(self, table, dialect, db):
22 |
23 | self._table = table
24 | self._db = db
25 | self._set = []
26 | WhereQuery.__init__(self, dialect)
27 |
28 | def table(self, table):
29 | self._table = table
30 | return self
31 |
32 | def execute(self):
33 | return self._db.execute(self.to_sql(), self.bind)
34 |
35 | def mset(self, kwargs):
36 | for c in kwargs.iteritems():
37 | self._set.append(c)
38 |
39 | return self
40 |
41 | def set(self, column, value):
42 | self._set.append((column, value))
43 | return self
44 |
45 |
46 | def compile(self):
47 | sql = 'UPDATE ' + self.dialect.quote_table(self._table)
48 |
49 | sql += ' SET ' + self.compile_set(self._set)
50 | if self._where:
51 | sql += ' WHERE ' + self.compile_condition(self._where)
52 | if self._order_by:
53 | sql += self.compile_order_by(self._where)
54 |
55 | if self._limit:
56 | sql += ' LIMIT ' + str(self._limit)
57 |
58 | return sql
59 |
60 | def clear(self):
61 | WhereQuery.clear(self)
62 | self._table = None
63 | self._set = []
64 | self._limit = None
65 | self.bind = []
--------------------------------------------------------------------------------
/db/query/delete.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.query.select import WhereQuery
17 |
18 |
19 | class DeleteQuery(WhereQuery):
20 | """Delete operator query builder"""
21 |
22 | def __init__(self, table, dialect, db):
23 | """Constructor
24 |
25 | :param table: table name
26 | :type table: str
27 | :param dialect: the sql dialect instance
28 | :param db: the database connection instance
29 | """
30 | if table:
31 | self._table = table
32 | self._db = db
33 | WhereQuery.__init__(self, dialect)
34 |
35 | def table(self, table):
36 | """Sets table name"""
37 | self._table = table
38 | return self
39 |
40 | def compile(self):
41 | """Compiles the delete sql statement"""
42 | sql = ''
43 | sql += 'DELETE FROM ' + self.dialect.quote_table(self._table)
44 | if self._where:
45 | sql += ' WHERE ' + self.compile_condition(self._where)
46 | if self._order_by:
47 | sql += ' ' + self.compile_order_by(self._order_by)
48 |
49 | if self._limit:
50 | sql += ' LIMIT ' + self._limit
51 | return sql
52 |
53 | def clear(self):
54 | """Clear and reset to orignal state"""
55 | WhereQuery.clear(self)
56 | self._table = None
57 | self._parameters = []
58 | self._sql = None
59 |
60 | def execute(self):
61 | """Execute the sql for delete operator"""
62 | return self._db.execute(self.to_sql(), self.bind)
63 |
--------------------------------------------------------------------------------
/db/mysql/connection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.connection import Connection
17 | from db.errors import (
18 | NotInstallDriverError,
19 | )
20 |
21 | try:
22 | import MySQLdb
23 | from MySQLdb.cursors import DictCursor, Cursor
24 | except ImportError:
25 | raise NotInstallDriverError("Must install MySQLdb module fistly")
26 |
27 |
28 | import time
29 | import logging
30 |
31 | LOGGER = logging.getLogger('db.mysql')
32 |
33 |
34 | class MySQLConnection(Connection):
35 |
36 | def initialize(self):
37 | self._last_used = time.time()
38 | self._max_idle = self._db_options.pop('max_idle', 10)
39 |
40 | def default_options(self):
41 | return {
42 | 'port': 3306,
43 | 'host': 'localhost',
44 | 'user': 'test',
45 | 'passwd': 'test',
46 | 'db': 'test',
47 | 'use_unicode': True,
48 | 'charset': 'utf8'
49 | }
50 |
51 | def connect(self):
52 | self.close()
53 | self._connect = MySQLdb.connect(**self._db_options)
54 | self._connect.autocommit(True)
55 |
56 | def ensure_connect(self):
57 | if not self._connect or self._max_idle < (time.time() - self._last_used):
58 | try:
59 | self._connect.ping()
60 | except:
61 | self.connect()
62 | self._last_used = time.time()
63 |
64 | def real_ctype(self, as_dict=False):
65 | if as_dict:
66 | return DictCursor
67 | return Cursor
68 |
69 | def driver(self):
70 | return 'mysql'
71 |
--------------------------------------------------------------------------------
/db/query/insert.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.query.base import Query
17 |
18 |
19 | class InsertQuery(Query):
20 |
21 | def __init__(self, table, dialect, db, columns=[]):
22 | self._columns = columns or []
23 | self._values = []
24 | self._table = table
25 | self._db = db
26 | Query.__init__(self, dialect)
27 |
28 | def table(self, table):
29 | self._table = table
30 | return self
31 |
32 | def fields(self, *columns):
33 | self._columns.extend(columns)
34 | return self
35 |
36 | def values(self, values):
37 | """The values for insert ,
38 | it can be a dict row or list tuple row.
39 | """
40 | if isinstance(values, dict):
41 | l = []
42 | for column in self._columns:
43 | l.append(values[column])
44 | self._values.append(tuple(l))
45 | else:
46 | self._values.append(values)
47 | return self
48 |
49 | def compile(self):
50 | sql = 'INSERT INTO ' + self.dialect.quote_table(self._table)
51 | if self._columns:
52 | sql += ' (' + ', '.join([self.dialect.quote_column(_) for _ in self._columns]) + ')'
53 | sql += ' VALUES(' + ', '.join(['%s' for _ in range(len(self._values[0]))]) + ')'
54 | return sql
55 |
56 | def execute(self):
57 | bind = self._values[0] if len(self._values) == 1 else self._values
58 | return self._db.execute(self.to_sql(), bind)
59 |
60 | def clear(self):
61 | self._columns = []
62 | self._values = []
63 | return self
64 |
--------------------------------------------------------------------------------
/db/pymysql/connection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.connection import Connection
17 | from db.errors import (
18 | NotInstallDriverError,
19 | )
20 |
21 | try:
22 | import pymysql
23 | pymysql.install_as_MySQLdb()
24 | from pymysql.cursors import DictCursor, Cursor
25 | except ImportError:
26 | raise NotInstallDriverError("Must install pymysql module fistly")
27 |
28 |
29 |
30 | import time
31 | import logging
32 |
33 | LOGGER = logging.getLogger('db.pymysql')
34 |
35 |
36 | class PyMySQLConnection(Connection):
37 |
38 | def initialize(self):
39 | self._last_used = time.time()
40 | self._max_idle = self._db_options.pop('max_idle', 10)
41 |
42 | def default_options(self):
43 | return {
44 | 'port': 3306,
45 | 'host': 'localhost',
46 | 'user': 'test',
47 | 'passwd': 'test',
48 | 'db': 'test',
49 | 'use_unicode': True,
50 | 'charset': 'utf8'
51 | }
52 |
53 | def connect(self):
54 | self.close()
55 | self._connect = pymysql.connect(**self._db_options)
56 | self._connect.autocommit(True)
57 |
58 | def ensure_connect(self):
59 | if not self._connect or self._max_idle < (time.time() - self._last_used):
60 | try:
61 | self._connect.ping()
62 | except:
63 | self.connect()
64 | self._last_used = time.time()
65 |
66 | def real_ctype(self, as_dict=False):
67 | if as_dict:
68 | return DictCursor
69 | return Cursor
70 |
71 | def driver(self):
72 | return 'pymysql'
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/samples/model.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from hashlib import sha224
17 | from datetime import datetime
18 |
19 |
20 | class User(object):
21 |
22 | def __init__(self, username, email, real_name, password, bio, status, role='user', uid=None):
23 | """If the user load from database, if will intialize the uid and secure password.
24 | Otherwise will hash encrypt the real password
25 |
26 | arg role enum: the string in ('user', 'editor', 'administrator')
27 | arg status enum: the string in ('actived', 'inactive')
28 | arg password fix legnth string: the use sha224 password hash
29 | """
30 |
31 | self.username = username
32 | self.email = email
33 | self.real_name = real_name
34 | self.bio = bio
35 | self.status = status
36 | self.role = role
37 |
38 | if uid:
39 | self.uid = uid
40 | self.password = password
41 | else:
42 | self.password = self.secure_password(password)
43 |
44 | def check(self, password):
45 | """Check the password"""
46 | return self.password == self.secure_password(password)
47 |
48 | def secure_password(self, password):
49 | """Encrypt password to sha224 hash"""
50 | return sha224(password).hexdigest()
51 |
52 | def as_json(self):
53 | data = self.__dict__.copy()
54 | del data['password']
55 | return data
56 |
57 |
58 | class Post(object):
59 |
60 | def __init__(self, title, slug, description, html, css, js, category, status,
61 | comments, author=None, created=datetime.now(), pid=None):
62 | self.title = title
63 | self.slug = slug
64 | self.description = description
65 | self.created = created
66 | self.html = html
67 | self.css = css
68 | self.js = js
69 | self.category = category
70 | self.status = status
71 | self.comments = comments
72 | self.author = author
73 | self.created = created
74 | self.pid = pid
75 |
76 | def as_json(self):
77 | data = self.__dict__.copy()
78 | return data
79 |
--------------------------------------------------------------------------------
/db/connection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 |
17 | class Connection(object):
18 | """Base Database Connection class
19 |
20 | :param db_options: db optional configuration, defaults to None
21 | :type db_options: dict, optional
22 | """
23 |
24 | def __init__(self, db_options=None):
25 | db_options = db_options or {}
26 |
27 | #: database optional configuration, defaults to None
28 | self._db_options = self.default_options()
29 | self._db_options.update(db_options)
30 |
31 | #: database real connection
32 | self._connect = None
33 | self.initialize()
34 |
35 | def initialize(self):
36 | """Initialize customize configuration in subclass"""
37 | pass
38 |
39 | def default_options(self):
40 | """Defalut options for intailize sql connection"""
41 | return {}
42 |
43 | def connect(self):
44 | """connects database"""
45 | raise NotImplementedError('Must implement connect in Subclass')
46 |
47 | def close(self):
48 | """Close connect"""
49 | if self._connect is not None:
50 | self._connect.close()
51 | self._connect = None
52 |
53 | def ensure_connect(self):
54 | """Ensure the connetion is useable"""
55 | raise NotImplementedError('Must implement ensure_connect in Subclass')
56 |
57 | def cursor(self, as_dict=False):
58 | """Gets the cursor by type , if ``as_dict is ture, make a dict sql connection cursor"""
59 | self.ensure_connect()
60 | ctype = self.real_ctype(as_dict)
61 | return self._connect.cursor(ctype)
62 |
63 | def real_ctype(self, as_dict):
64 | """The real sql cursor type"""
65 | raise NotImplementedError('Must implement real_ctype in Subclass')
66 |
67 | def driver(self):
68 | """Get database driver"""
69 | return None
70 |
71 | def commit(self):
72 | """Commit batch execute"""
73 | self._connect.commit()
74 |
75 | def rollback(self):
76 | """Rollback database process"""
77 | self._connect.rollback()
78 |
79 | def autocommit(self, enable=True):
80 | """Sets commit to auto if True"""
81 | self._connect.autocommit(enable)
82 |
--------------------------------------------------------------------------------
/db/pool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import threading
17 | import logging
18 | from db.errors import DBError
19 | import time
20 |
21 |
22 | LOGGER = logging.getLogger('db.pool')
23 |
24 | class BaseConnectionPool(object):
25 |
26 | def __init__(self, minconn, maxconn, connection_cls, db_options={}):
27 | self.db_options = db_options
28 | self.maxconn = maxconn
29 | self.minconn = minconn if self.maxconn > minconn else int(self.maxconn * 0.2)
30 | if not connection_cls:
31 | raise ValueError('Must be Connection subclass')
32 | self.connection_cls = connection_cls
33 |
34 | def new_connect(self):
35 | return self.connection_cls(self.db_options)
36 |
37 | def push(self, con):
38 | pass
39 |
40 | def pop(self):
41 | pass
42 |
43 | def release(self):
44 | pass
45 |
46 |
47 | class ConnectionPool(BaseConnectionPool):
48 |
49 | def __init__(self, minconn=3, maxconn=10,connection_cls=None, db_options={}):
50 | self._created_conns = 0
51 | BaseConnectionPool.__init__(self, minconn, maxconn, connection_cls, db_options)
52 | self._lock = threading.Lock()
53 | self._available_conns = []
54 | self._in_use_conns = []
55 | for i in range(self.minconn):
56 | self._available_conns.append(self.new_connect())
57 |
58 | def pop(self):
59 | con = None
60 | first_tried = time.time()
61 | while True:
62 | self._lock.acquire()
63 | try:
64 | con = self._available_conns.pop(0)
65 | self._in_use_conns.append(con)
66 | break
67 | except IndexError:
68 |
69 | if self._created_conns < self.maxconn:
70 |
71 | self._created_conns += 1
72 | con = self.new_connect()
73 | self._in_use_conns.append(con)
74 | break
75 | finally:
76 | self._lock.release()
77 |
78 | if not con and 3 <= (time.time() - first_tried):
79 | raise DBPoolError("tried 3 seconds, can't load connection, maybe too many threads")
80 |
81 | return con
82 |
83 | def push(self, con):
84 | self._lock.acquire()
85 | if con in self._in_use_conns:
86 | self._in_use_conns.remove(con)
87 | self._available_conns.append(con)
88 | self._lock.release()
89 |
90 | def release(self):
91 | with self._lock:
92 | for conn in self._available_conns:
93 | conn.close()
94 | for conn in self._in_use_conns:
95 | conn.close()
96 | self._available_conns[:] = []
97 | self._in_use_conns[:] = []
98 | self._created_conns = 0
99 |
100 |
101 | class DBPoolError(DBError): pass
102 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | tmp/
9 | *.tmp
10 | *.bak
11 | *.swp
12 | *~.nib
13 | local.properties
14 | .classpath
15 | .settings/
16 | .loadpath
17 |
18 | # External tool builders
19 | .externalToolBuilders/
20 |
21 | # Locally stored "Eclipse launch configurations"
22 | *.launch
23 |
24 | # CDT-specific
25 | .cproject
26 |
27 | # PDT-specific
28 | .buildpath
29 |
30 |
31 | #################
32 | ## Visual Studio
33 | #################
34 |
35 | ## Ignore Visual Studio temporary files, build results, and
36 | ## files generated by popular Visual Studio add-ons.
37 |
38 | # User-specific files
39 | *.suo
40 | *.user
41 | *.sln.docstates
42 |
43 | # Build results
44 |
45 | [Dd]ebug/
46 | [Rr]elease/
47 | x64/
48 | build/
49 | [Oo]bj/
50 |
51 | # MSTest test Results
52 | [Tt]est[Rr]esult*/
53 | [Bb]uild[Ll]og.*
54 |
55 | *_i.c
56 | *_p.c
57 | *.ilk
58 | *.meta
59 | *.obj
60 | *.pch
61 | *.pdb
62 | *.pgc
63 | *.pgd
64 | *.rsp
65 | *.sbr
66 | *.tlb
67 | *.tli
68 | *.tlh
69 | *.tmp
70 | *.tmp_proj
71 | *.log
72 | *.vspscc
73 | *.vssscc
74 | .builds
75 | *.pidb
76 | *.log
77 | *.scc
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opensdf
84 | *.sdf
85 | *.cachefile
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 |
92 | # Guidance Automation Toolkit
93 | *.gpState
94 |
95 | # ReSharper is a .NET coding add-in
96 | _ReSharper*/
97 | *.[Rr]e[Ss]harper
98 |
99 | # TeamCity is a build add-in
100 | _TeamCity*
101 |
102 | # DotCover is a Code Coverage Tool
103 | *.dotCover
104 |
105 | # NCrunch
106 | *.ncrunch*
107 | .*crunch*.local.xml
108 |
109 | # Installshield output folder
110 | [Ee]xpress/
111 |
112 | # DocProject is a documentation generator add-in
113 | DocProject/buildhelp/
114 | DocProject/Help/*.HxT
115 | DocProject/Help/*.HxC
116 | DocProject/Help/*.hhc
117 | DocProject/Help/*.hhk
118 | DocProject/Help/*.hhp
119 | DocProject/Help/Html2
120 | DocProject/Help/html
121 |
122 | # Click-Once directory
123 | publish/
124 |
125 | # Publish Web Output
126 | *.Publish.xml
127 | *.pubxml
128 |
129 | # NuGet Packages Directory
130 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
131 | #packages/
132 |
133 | # Windows Azure Build Output
134 | csx
135 | *.build.csdef
136 |
137 | # Windows Store app package directory
138 | AppPackages/
139 |
140 | # Others
141 | sql/
142 | *.Cache
143 | ClientBin/
144 | [Ss]tyle[Cc]op.*
145 | ~$*
146 | *~
147 | *.dbmdl
148 | *.[Pp]ublish.xml
149 | *.pfx
150 | *.publishsettings
151 |
152 | # RIA/Silverlight projects
153 | Generated_Code/
154 |
155 | # Backup & report files from converting an old project file to a newer
156 | # Visual Studio version. Backup files are not needed, because we have git ;-)
157 | _UpgradeReport_Files/
158 | Backup*/
159 | UpgradeLog*.XML
160 | UpgradeLog*.htm
161 |
162 | # SQL Server files
163 | App_Data/*.mdf
164 | App_Data/*.ldf
165 |
166 | #############
167 | ## Windows detritus
168 | #############
169 |
170 | # Windows image file caches
171 | Thumbs.db
172 | ehthumbs.db
173 |
174 | # Folder config file
175 | Desktop.ini
176 |
177 | # Recycle Bin used on file shares
178 | $RECYCLE.BIN/
179 |
180 | # Mac crap
181 | .DS_Store
182 |
183 |
184 | #############
185 | ## Python
186 | #############
187 |
188 | *.py[co]
189 |
190 | # Packages
191 | *.egg
192 | *.egg-info
193 | dist/
194 | build/
195 | eggs/
196 | parts/
197 | var/
198 | sdist/
199 | develop-eggs/
200 | .installed.cfg
201 |
202 | # Installer logs
203 | pip-log.txt
204 |
205 | # Unit test / coverage reports
206 | .coverage
207 | .tox
208 |
209 | #Translations
210 | *.mo
211 |
212 | #Mr Developer
213 | .mr.developer.cfg
214 |
--------------------------------------------------------------------------------
/db/dialect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.query.select import SelectQuery
17 | from db.query.base import Query
18 | from db.query.expr import Expr
19 | from db.query.update import UpdateQuery
20 | from db.query.insert import InsertQuery
21 | from db.query.delete import DeleteQuery
22 |
23 | class Dialect(object):
24 |
25 | def __init__(self, db=None):
26 | self._identifier = '"'
27 | self.db = db
28 | self.initialize()
29 |
30 | def initialize(self):
31 | pass
32 |
33 | def select(self, table):
34 | return SelectQuery(table, self, self.db)
35 |
36 | def update(self, table):
37 | return UpdateQuery(table, self, self.db)
38 |
39 | def insert(self, table):
40 | return InsertQuery(table, self, self.db)
41 |
42 | def delete(self, table):
43 | return DeleteQuery(table, self, self.db)
44 |
45 | def escape(self):
46 | pass
47 |
48 | def quote(self, value):
49 | if not value:
50 | return 'NULL'
51 | elif value == True:
52 | return "'1'"
53 | elif value == False:
54 | return "'0'"
55 | elif isinstance(value, Query):
56 | if isinstance(value, SelectQuery):
57 | return '(' + value.compile() + ')'
58 | elif isinstance(value, Expr):
59 | return value.compile()
60 | else:
61 | return self.quote(value)
62 | elif isinstance(value, list):
63 | return '(' + ', '.join([self.quote(_) for _ in vlaue]) + ')'
64 | else:
65 | return str(value)
66 | return self.escape(value)
67 |
68 | def quote_column(self, column):
69 | alias = None
70 | escaped_identifier = self._identifier + self._identifier
71 | if isinstance(column, list):
72 | column, alias = column
73 | alias = alias.replace(self._identifier, escaped_identifier)
74 |
75 | if isinstance(column, Query):
76 | column = '(' + column.compile() + ')'
77 | elif isinstance(column, Expr):
78 | column = column.compile(self)
79 | else:
80 | column = str(column)
81 | column = column.replace(self._identifier, escaped_identifier)
82 | if column == '*':
83 | return column
84 | elif column.find('.') != -1:
85 | parts = column.split('.')
86 | _parts = []
87 | for part in parts:
88 | if part != '*':
89 | _parts.append(self._identifier + part + self._identifier)
90 | else:
91 | _parts.append(part)
92 | column = '.'.join(_parts)
93 | elif 'as' not in column.lower():
94 | column = self._identifier + column + self._identifier
95 |
96 | if alias:
97 | column += ' AS ' + self._identifier + alias + self._identifier
98 | return column
99 |
100 | def quote_table(self, table):
101 | alias = None
102 | escaped_identifier = self._identifier + self._identifier
103 | if isinstance(table, list):
104 | table, alias = table
105 | alias = alias.replace(self._identifier, escaped_identifier)
106 | if isinstance(table, Query):
107 | table = '(' + table.compile() + ')'
108 |
109 | elif isinstance(table, Expr):
110 | table = table.compile()
111 | else:
112 | table = table.replace(self._identifier, escaped_identifier)
113 | if table.find('.') != -1:
114 | parts = table.split('.')
115 | parts = [self._identifier + part + self._identifier for part in parts]
116 | table = '.'.join(parts)
117 | else:
118 | table = self._identifier + table + self._identifier
119 |
120 | if alias:
121 | table += ' AS ' + self._identifier + alias + self._identifier
122 |
123 | return table
124 |
125 | def quote_identifier(self, value):
126 | escaped_identifier = self._identifier + self._identifier
127 |
128 | if isinstance(value, list):
129 | value, alias = value
130 | alias = alias.replace(self._identifier, escaped_identifier)
131 | if isinstance(value, Query):
132 | value = '(' + value.compile() + ')'
133 |
134 | elif isinstance(value, Expr):
135 | value = value.compile()
136 | else:
137 | value = value.replace(self._identifier, escaped_identifier)
138 | if value.find('.') != -1:
139 | parts = value.split('.')
140 | parts = [self._identifier + part + self._identifier for part in parts]
141 | value = '.'.join(parts)
142 | else:
143 | value = self._identifier + value + self._identifier
144 | if alias:
145 | value += ' AS ' + self._identifier + alias + self._identifier
146 |
147 | return value
148 |
149 | def map_condition_operator(self, operator):
150 | return None
--------------------------------------------------------------------------------
/db/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | __version__ = '0.1.3'
17 | VERSION = tuple(map(int, __version__.split('.')))
18 |
19 |
20 | __all__ = [
21 | 'and_',
22 | 'or_',
23 | 'expr',
24 | 'query',
25 | 'execute',
26 | 'transaction',
27 | 'setup',
28 | 'select',
29 | 'insert',
30 | 'update',
31 | 'delete',
32 | 'database',
33 | 'DBError'
34 | ]
35 |
36 | from db.query.select import QueryCondition
37 | from db.query.expr import Expr as expr
38 | from db._db import DB
39 | from db.errors import DBError
40 | from random import choice
41 |
42 |
43 | def and_():
44 | return QueryCondition('AND')
45 |
46 |
47 | def or_():
48 | return QueryCondition('OR')
49 |
50 |
51 | __db = {}
52 |
53 |
54 | def setup(config, minconn=5, maxconn=10, adapter='mysql', key='default', slave=False):
55 | """Setup database
56 |
57 | :param config dict: is the db adapter config
58 | :param key string: the key to identify dabtabase
59 | :param adapter string: the dabtabase adapter current support mysql only
60 | :param minconn int: the min connection for connection pool
61 | :param maxconn int: the max connection for connection pool
62 | :param slave boolean: If True the database can be read only.
63 |
64 |
65 | """
66 | global __db
67 |
68 | if '.' in key:
69 | raise TypeError('The DB Key: "%s" Can\'t Contain dot' % (key))
70 |
71 | if slave == False and key in __db:
72 | raise DBError('The Key: "%s" was set' % (key))
73 |
74 | database = DB(config, minconn, maxconn, key, adapter)
75 |
76 | master_key = key
77 | slave_key = key + '.slave'
78 |
79 | if not slave:
80 | __db[master_key] = database
81 | if slave_key not in __db:
82 | __db[slave_key] = [database]
83 | else:
84 | if key in __db:
85 | databases = __db[slave_key]
86 | if len(databases) == 1 and __db[master_key] == databases[0]:
87 | __db[slave_key] = [database]
88 | else:
89 | __db[slave_key].append(database)
90 | else:
91 | __db[slave_key] = [database]
92 |
93 |
94 | def query(sql, args=None, many=None, as_dict=False, key='default'):
95 | """The connection raw sql query, when select table, show table
96 | to fetch records, it is compatible the dbi execute method::
97 |
98 |
99 | :param sql string: the sql stamtement like 'select * from %s'
100 | :param args list: Wen set None, will use dbi execute(sql), else
101 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list
102 | :param many int: when set, the query method will return genarate an iterate
103 | :param as_dict bool: when is True, the type of row will be dict, otherwise is tuple
104 | :param key: a key for your dabtabase you wanna use
105 | """
106 | database = choice(__db[key + '.slave'])
107 | return database.query(sql, args, many, as_dict)
108 |
109 |
110 | def execute(sql, args=None, key='default'):
111 | """It is used for update, delete records.
112 |
113 | :param sql string: the sql stamtement like 'select * from %s'
114 | :param args list: Wen set None, will use dbi execute(sql), else
115 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list
116 | :param key: a key for your dabtabase you wanna use
117 |
118 | eg::
119 |
120 | execute('insert into users values(%s, %s)', [(1L, 'blablabla'), (2L, 'animer')])
121 | execute('delete from users')
122 | """
123 | database = __db[key]
124 | return database.execute(sql, args)
125 |
126 |
127 | def transaction(key='default'):
128 | """transaction wrapper
129 |
130 | :param key: a key for your dabtabase you wanna use
131 | """
132 | database = __db[key]
133 | return database.transaction()
134 |
135 |
136 | def select(table, key='default'):
137 | """Select dialect
138 |
139 |
140 | :param key: a key for your dabtabase you wanna use
141 | """
142 | database = choice(__db[key + '.slave'])
143 | return database.select(table)
144 |
145 |
146 | def insert(table, key='default'):
147 | """insert dialect
148 |
149 | :param key: a key for your dabtabase you wanna use
150 | """
151 | database = __db[key]
152 | return database.insert(table)
153 |
154 |
155 | def update(table, key='default'):
156 | """update dialect
157 |
158 | :param key: a key for your dabtabase you wanna use
159 | """
160 | database = __db[key]
161 | return database.update(table)
162 |
163 |
164 | def delete(table, key='default'):
165 | """delete dialect
166 |
167 | :param key: a key for your dabtabase you wanna use
168 | """
169 | database = __db[key]
170 | return database.delete(table)
171 |
172 |
173 | def database(key='default', slave=False):
174 | """datbase dialect
175 |
176 | :param key: a key for your dabtabase you wanna use
177 | :param slave boolean: If True the database can be read only, Defaults False.
178 | """
179 | if slave:
180 | key += '.slave'
181 | return choice(__db[key])
182 | return __db.get(key)
183 |
--------------------------------------------------------------------------------
/samples/orm.py:
--------------------------------------------------------------------------------
1 | # !/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 |
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 |
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 | import db
18 |
19 | from model import User, Post
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 |
24 | class BaseMapper(object):
25 |
26 | def load(self, data, o):
27 | return o(*data)
28 |
29 |
30 | class PrimaryTrait(object):
31 |
32 | primary_id = 'id'
33 |
34 | def find(self, id):
35 | q = db.select(self.table).condition(self.primary_id, id)
36 | data = q.query()
37 | if data:
38 | return self.load(data[0], self.model)
39 |
40 |
41 | class UserMapper(BaseMapper, PrimaryTrait):
42 |
43 | model = User
44 | table = 'users'
45 |
46 | def find(self, uid):
47 | """Find and load the user from database by uid(user id)"""
48 | data = (db.select(self.table).select('username', 'email', 'real_name',
49 | 'password', 'bio', 'status', 'role', 'uid').
50 | condition('uid', uid).execute()
51 | )
52 | if data:
53 | logger.info('data %s', data)
54 | return self.load(data[0], self.model)
55 |
56 | def find_by_username(self, username):
57 | """Return user by username if find in database otherwise None"""
58 | data = (db.select(self.table).select('username', 'email', 'real_name',
59 | 'password', 'bio', 'status', 'role', 'uid').
60 | condition('username', username).execute()
61 | )
62 | if data:
63 | return self.load(data[0], self.model)
64 |
65 | def create(self, user):
66 | return db.execute("INSERT INTO users(username, email, real_name, password, bio, status, role) \
67 | VALUES(%s, %s, %s, %s, %s, %s, %s)",
68 | (user.username, user.email, user.real_name, user.password, user.bio, user.status, user.role))
69 |
70 | def search(self, **kw):
71 | """Find the users match the condition in kw"""
72 | q = db.select(self.table).condition('status', 'active')
73 | for k, v in kw:
74 | q.condition(k, v)
75 | data = q.execute()
76 | users = []
77 | for user in data:
78 | users.append(self.load(user, self.model))
79 | return users
80 |
81 | def count(self):
82 | return db.query('SELECT COUNT(*) FROM ' + self.table)[0][0]
83 |
84 | def paginate(self, page=1, perpage=10):
85 | count = self.count()
86 | q = db.select(self.table).select('username', 'email', 'real_name',
87 | 'password', 'bio', 'status', 'role', 'uid')
88 | results = q.limit(perpage).offset((page - 1) * perpage).order_by('real_name', 'desc').execute()
89 | return [self.load(user, self.model) for user in results]
90 |
91 | def save(self, user):
92 | q = db.update(self.table)
93 | data = dict((_, getattr(user, _)) for _ in ('username', 'email', 'real_name',
94 | 'password', 'bio', 'status', 'role'))
95 | q.mset(data)
96 | return q.condition('uid', user.uid).execute()
97 |
98 | def delete(self, user):
99 | return db.delete(self.table).condition('uid', user.uid).execute()
100 |
101 |
102 | class PostMapper(BaseMapper):
103 |
104 | table = 'posts'
105 | model = Post
106 |
107 | def find(self, pid):
108 | data = db.select(self.table).fields('title', 'slug', 'description', 'html', 'css', 'js',
109 | 'category', 'status', 'comments', 'author', 'created', 'pid').condition('pid', pid).execute()
110 | if data:
111 | return self.load(data[0], self.model)
112 |
113 | def count(self):
114 | return db.select(self.table).fields(db.expr('COUNT(*)')).execute()[0][0]
115 |
116 | def create(self, post):
117 | row = []
118 | for _ in ('title', 'slug', 'description', 'created', 'html', 'css', 'js',
119 | 'category', 'status', 'comments', 'author'):
120 | row.append(getattr(post, _))
121 | return db.insert(self.table).columns('title', 'slug', 'description', 'created', 'html', 'css', 'js',
122 | 'category', 'status', 'comments', 'author').values(row).execute()
123 |
124 | def paginate(self, page=1, perpage=10, category=None):
125 | """Paginate the posts"""
126 | q = db.select(self.table).fields('title', 'slug', 'description', 'html', 'css', 'js',
127 | 'category', 'status', 'comments', 'author', 'created', 'pid')
128 | if category:
129 | q.condition('category', category)
130 | results = (q.limit(perpage).offset((page - 1) * perpage)
131 | .order_by('created', 'DESC').execute())
132 | return [self.load(data, self.model) for data in results]
133 |
134 | def save(self, page):
135 | q = db.update(self.table)
136 | data = dict((_, getattr(page, _)) for _ in ('title', 'slug', 'description', 'html', 'css', 'js',
137 | 'category', 'status', 'comments'))
138 | q.mset(data)
139 | return q.condition('pid', page.pid).execute()
140 |
141 | def delete(self, page_id):
142 | return db.delete(self.table).condition('pid', page_id).execute()
143 |
144 | def category_count(self, category_id):
145 | return db.select(self.table).fields(db.expr('count(*)',
146 | 'total')).condition('category', category_id).condition('status', 'published').execute()[0][0]
147 |
148 |
149 | __backends = {}
150 | __backends['post'] = PostMapper()
151 | __backends['user'] = UserMapper()
152 |
153 |
154 | def Backend(name):
155 | return __backends.get(name)
156 |
--------------------------------------------------------------------------------
/db/_db.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import sys
17 | import logging
18 | from db.errors import DBError
19 | from db.pool import ConnectionPool
20 |
21 | LOGGER = logging.getLogger('db')
22 |
23 |
24 | class DB(object):
25 |
26 | adapters = {}
27 | dialects = {}
28 |
29 | def __init__(self, config, minconn=5, maxconn=10, key='defalut', adapter='mysql'):
30 | """ Setup DB::
31 |
32 | param config dict: is the db adapter config
33 | param key string: the key to identify dabtabase
34 | param adapter string: the dabtabase adapter current support mysql only
35 | param minconn int: the min connection for connection pool
36 | param maxconn int: the max connection for connection pool
37 |
38 | """
39 | adapter = adapter or 'mysql'
40 | self.key = key
41 | self.adapter = adapter
42 | self.pool = ConnectionPool(minconn, maxconn, self.connection_class(adapter), config)
43 | self.dialect = self.dialect_class(adapter)(self)
44 |
45 | def select(self, table):
46 | """Select sql executor
47 |
48 |
49 | :param table: table name
50 | :type table: str
51 | :returns: select query instance
52 | """
53 | return self.dialect.select(table)
54 |
55 | def insert(self, table):
56 | """insert sql executor
57 |
58 |
59 | :param table: table name
60 | :type table: str
61 | :returns: insert query instance
62 | """
63 | return self.dialect.insert(table)
64 |
65 | def update(self, table):
66 | """update sql executor
67 |
68 |
69 | :param table: table name
70 | :type table: str
71 | :returns: update query instance
72 | """
73 | return self.dialect.update(table)
74 |
75 | def delete(self, table):
76 | """delete sql executor
77 |
78 |
79 | :param table: table name
80 | :type table: str
81 | :returns: delete query instance
82 | """
83 | return self.dialect.delete(table)
84 |
85 | def query(self, sql, args=None, many=None, as_dict=False):
86 | """The connection raw sql query, when select table, show table
87 | to fetch records, it is compatible the dbi execute method.
88 |
89 |
90 | :param sql string: the sql stamtement like 'select * from %s'
91 | :param args list: Wen set None, will use dbi execute(sql), else
92 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list
93 | :param many int: when set, the query method will return genarate an iterate
94 | :param as_dict bool: when is true, the type of row will be dict, otherwise is tuple
95 | """
96 | con = self.pool.pop()
97 | c = None
98 | try:
99 | c = con.cursor(as_dict)
100 | LOGGER.debug("Query sql: " + sql + " args:" + str(args))
101 | c.execute(sql, args)
102 | if many and many > 0:
103 | return self._yield(con, c, many)
104 | else:
105 | return c.fetchall()
106 |
107 | except Exception as e:
108 | LOGGER.error("Error Qeury on %s", str(e))
109 | raise DBError(e.args[0], e.args[1])
110 | finally:
111 | many or (c and c.close())
112 | many or (con and self.pool.push(con))
113 |
114 | def _yield(self, con, cursor, many):
115 | try:
116 | result = cursor.fetchmany(many)
117 | while result:
118 | for row in result:
119 | yield row
120 | result = cursor.fetchmany(many)
121 | finally:
122 | cursor and cursor.close()
123 | con and self.pool.push(con)
124 |
125 | def execute(self, sql, args=None):
126 | """It is used for update, delete records.
127 |
128 | :param sql string: the sql stamtement like 'select * from %s'
129 | :param args list: Wen set None, will use dbi execute(sql), else
130 | dbi execute(sql, args), the args keep the original rules, it shuld be tuple or list of list
131 |
132 | eg::
133 |
134 | execute('insert into users values(%s, %s)', [(1L, 'blablabla'), (2L, 'animer')])
135 | execute('delete from users')
136 | """
137 | con = self.pool.pop()
138 | c = None
139 | try:
140 | c = con.cursor()
141 | LOGGER.debug("Execute sql: " + sql + " args:" + str(args))
142 | if type(args) is tuple:
143 | c.execute(sql, args)
144 | elif type(args) is list:
145 | if len(args) > 1 and type(args[0]) in (list, tuple):
146 | c.executemany(sql, args)
147 | else:
148 | c.execute(sql, args)
149 | elif args is None:
150 | c.execute(sql)
151 | if sql.lstrip()[:6].upper() == 'INSERT':
152 | return c.lastrowid
153 | return c.rowcount
154 | except Exception as e:
155 | LOGGER.error("Error Execute on %s", str(e))
156 | raise DBError(str(e))
157 | finally:
158 | c and c.close()
159 | con and self.pool.push(con)
160 |
161 | def transaction(self):
162 | return Transaction(self)
163 |
164 | def connection_class(self, adapter):
165 | """Get connection class by adapter"""
166 | if self.adapters.get(adapter):
167 | return self.adapters[adapter]
168 | try:
169 | class_prefix = getattr(
170 | __import__('db.' + adapter, globals(), locals(),
171 | ['__class_prefix__']), '__class_prefix__')
172 | driver = self._import_class('db.' + adapter + '.connection.' +
173 | class_prefix + 'Connection')
174 | except ImportError:
175 | raise DBError("Must install adapter `%s` or doesn't support" %
176 | (adapter))
177 |
178 | self.adapters[adapter] = driver
179 | return driver
180 |
181 | def dialect_class(self, adapter):
182 | """Get dialect sql class by adapter"""
183 | if self.dialects.get(adapter):
184 | return self.dialects[adapter]
185 | try:
186 | class_prefix = getattr(
187 | __import__('db.' + adapter, globals(), locals(),
188 | ['__class_prefix__']), '__class_prefix__')
189 | driver = self._import_class('db.' + adapter + '.dialect.' +
190 | class_prefix + 'Dialect')
191 | except ImportError:
192 | raise DBError("Must install adapter `%s` or doesn't support" %
193 | (adapter))
194 |
195 | self.dialects[adapter] = driver
196 | return driver
197 |
198 | def _import_class(self, module2cls):
199 | """Import class by module dot split string"""
200 | d = module2cls.rfind(".")
201 | classname = module2cls[d + 1: len(module2cls)]
202 | m = __import__(module2cls[0:d], globals(), locals(), [classname])
203 | return getattr(m, classname)
204 |
205 |
206 | class lazy_attr(object):
207 |
208 | def __init__(self, wrapped):
209 | self.wrapped = wrapped
210 | try:
211 | self.__doc__ = wrapped.__doc__
212 | except: # pragma: no cover
213 | pass
214 |
215 | def __get__(self, inst, objtype=None):
216 | if inst is None:
217 | return self
218 | val = self.wrapped(inst)
219 | setattr(inst, self.wrapped.__name__, val)
220 | return val
221 |
222 |
223 | class Transaction(object):
224 | """Database sql Transaction"""
225 |
226 | def __init__(self, db):
227 | self._db = db
228 | self._con = None
229 |
230 | @lazy_attr
231 | def dialect(self):
232 | return self._db.dialects.get(self._db.adapter)(self)
233 |
234 | def __enter__(self):
235 | self._con = self._db.pool.pop()
236 | self._con.ensure_connect()
237 | self._con.autocommit(False)
238 | return self
239 |
240 | def __exit__(self, exc_type, exc_value, traceback):
241 | try:
242 | self._con.commit()
243 | self._con.autocommit(True)
244 | except Exception as e:
245 | try:
246 | self._con.rollback()
247 | except Exception as e_:
248 | LOGGER.error('When transaction happend error: %s', e_)
249 | raise e
250 | finally:
251 | self._db.pool.push(self._con)
252 | self._con = None
253 | self._db = None
254 |
255 | def begin(self):
256 | """Begins transaction"""
257 | self._con = self._db.pool.pop()
258 | self._con.ensure_connect()
259 | self._con.autocommit(False)
260 |
261 | def commit(self):
262 | """Commits transaction"""
263 | try:
264 | self._con.commit()
265 | self._con.autocommit(True)
266 | except Exception as e:
267 | try:
268 | self._con.rollback()
269 | except Exception as e_:
270 | LOGGER.error('When transaction happend error: %s', e_)
271 | raise e
272 | finally:
273 | self._db.pool.push(self._con)
274 | self._con = None
275 | self._db = None
276 |
277 | def execute(self, sql, args):
278 | """Execute sql
279 |
280 | :param sql string: the sql stamtement like 'select * from %s'
281 | :param args list: Wen set None, will use dbi execute(sql), else
282 | db execute(sql, args), the args keep the original rules, it shuld be tuple or list of list
283 | """
284 | c = None
285 | try:
286 | c = self._con.cursor()
287 | LOGGER.debug("execute sql: " + sql + " args:" + str(args))
288 | if type(args) is tuple:
289 | c.execute(sql, args)
290 | elif type(args) is list:
291 | if len(args) > 1 and type(args[0]) in (list, tuple):
292 | c.executemany(sql, args)
293 | else:
294 | c.execute(sql, args)
295 | elif args is None:
296 | c.execute(sql)
297 | if sql.lstrip()[:6].upper() == 'INSERT':
298 | return c.lastrowid
299 | return c.rowcount
300 | finally:
301 | c and c.close()
302 |
303 | def insert(self, table):
304 | """Insert sql diaect"""
305 | return self.dialect.insert(table)
306 |
307 | def update(self, table):
308 | """update sql diaect"""
309 | return self.dialect.update(table)
310 |
311 | def delete(self, table):
312 | """delete sql diaect"""
313 | return self.dialect.delete(table)
314 |
--------------------------------------------------------------------------------
/db/query/select.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from db.query.base import Query
17 | from db.errors import DBError
18 | from db.query import LOGGER
19 |
20 |
21 | class QueryCondition(object):
22 |
23 | def __init__(self, glue):
24 | self.glue = glue
25 | self.conditions = []
26 | self.changed = True
27 |
28 | self.bind = []
29 |
30 | def __nonzero__(self):
31 | return bool(len(self.conditions))
32 | __bool__ = __nonzero__
33 |
34 | def where(self, fields, snippet):
35 | self.conditions.append(dict(
36 | field=fields,
37 | value=snippet,
38 | op=None
39 | ))
40 | self.changed = True
41 | return self
42 |
43 | def __len__(self):
44 | return len(self.conditions)
45 |
46 | def condition(self, field, value=None, op=None):
47 | if not op:
48 | if isinstance(value, (list, tuple)):
49 | op = 'IN'
50 | elif value is None:
51 | op = 'IS NULL'
52 | else:
53 | op = '='
54 | self.conditions.append(dict(
55 | field=field,
56 | value=value,
57 | op=op
58 | ))
59 | self.changed = True
60 |
61 | return self
62 |
63 | def is_null(self, field):
64 | return self.condition(field)
65 |
66 | def is_not_null(self, field):
67 | return self.condition(field, None, 'IS NOT NULL')
68 |
69 | def exists(self, select):
70 | return self.condition('', select, 'EXISTS')
71 |
72 | def not_exists(self, select):
73 | return self.condition('', select, 'NOT EXISTS')
74 |
75 | def compile(self, db, query_placeholder='%s'):
76 | if self.changed:
77 | condition_fragments = []
78 | #arguments = []
79 | conditions = [_.copy() for _ in self.conditions]
80 | glue = self.glue
81 | self.glue = None
82 |
83 | for condition in conditions:
84 | is_select = False
85 | if not condition.get('op'):
86 | condition_fragments.append(' (' + condition['field'] + ') ')
87 | else:
88 | field = condition['field']
89 | if isinstance(field, QueryCondition):
90 | field.compile(db)
91 | if len(condition) == 1:
92 | condition_fragments.append(field.to_sql())
93 | else:
94 | condition_fragments.append(
95 | ' (' + field.to_sql() + ') ')
96 | self.bind.extend(field.bind)
97 | else:
98 | operator_defaults = dict(
99 | prefix='',
100 | postfix='',
101 | delimiter='',
102 | op=condition['op'],
103 | use_value=True)
104 | op = db.map_condition_operator(condition['op'])
105 | if not op:
106 | op = self.map_condition_operator(
107 | condition['op'])
108 | operator_defaults.update(op)
109 | op = operator_defaults
110 | placeholders = []
111 | if isinstance(condition['value'], SelectQuery):
112 | is_select = True
113 | condition['value'].compile(
114 | db, query_placeholder)
115 | placeholders.append(condition['value'].to_sql())
116 | self.bind.extend(condition.bind)
117 | op['use_value'] = False
118 | elif not op.get('delimiter'):
119 | condition['value'] = [condition['value']]
120 |
121 | if op.get('use_value'):
122 | for value in condition['value']:
123 | if isinstance(value, list):
124 | placeholders.append(
125 | ', '.join(['%s'] * len(value)))
126 | self.bind.extend(value)
127 | else:
128 | self.bind.append(value)
129 | placeholders.append('%s')
130 |
131 | if is_select:
132 | LOGGER.deubg('select :' + db.quote_column(condition['field']))
133 | condition_fragments.append(' (' +
134 | db.quote_column(condition['field']) + ' ' +
135 | op['op'] + ' ' + op['prefix'] +
136 | op['delimiter'].join(placeholders) + op['postfix'] + ') ')
137 | else:
138 | condition_fragments.append(' ' +
139 | db.quote_column(condition['field']) + ' ' +
140 | op['op'] + ' ' + op['prefix'] +
141 | op['delimiter'].join(placeholders) + op['postfix'] + ' ')
142 |
143 | self.changed = False
144 | self.string_version = glue.join(condition_fragments)
145 |
146 | def to_sql(self):
147 | if self.changed:
148 | return ''
149 | return self.string_version
150 | __str__ = to_sql
151 |
152 | def map_condition_operator(self, op):
153 | specials = {
154 | 'BETWEEN': {'delimiter': ' AND '},
155 | 'IN': {'delimiter': ', ', 'prefix': ' (', 'postfix': ') '},
156 | 'NOT IN': {'delimiter': ', ', 'prefix': ' (', 'postfix': ')'},
157 | 'EXISTS': {'prefix': ' (', 'postfix': ') '},
158 | 'NOT EXISTS': {'prefix': ' (', 'postfix': ') '},
159 | 'IS NULL': {'use_value': False},
160 | 'IS NOT NULL': {'use_value': False},
161 | 'LIKE': {},
162 | 'NOT LIKE': {},
163 | '=': {},
164 | '<': {},
165 | '>': {},
166 | '>=': {},
167 | '<=': {}
168 | }
169 | if specials.get(op):
170 | result = specials.get(op)
171 | else:
172 | op = op.upper()
173 | result = specials.get(op) if specials.get(op) else {}
174 |
175 | result['op'] = op
176 | return result
177 |
178 |
179 | class WhereQuery(Query):
180 |
181 | def __init__(self, dialect):
182 | self._where = QueryCondition('AND')
183 | self._order_by = []
184 | self._limit = None
185 |
186 | self.bind = []
187 |
188 | Query.__init__(self, dialect)
189 |
190 | def where(self, snippet, args):
191 | self._where.where(snippet, args)
192 | return self
193 |
194 | def condition(self, field, value=None, operator=None):
195 | self._where.condition(field, value, operator)
196 | return self
197 |
198 | def conditions(self):
199 | return self._where.conditions()
200 |
201 | def arguments(self):
202 | return self._where.arguments()
203 |
204 | def is_null(self, field):
205 | self._where.is_null(field)
206 | return self
207 |
208 | def is_not_null(self, field):
209 | self._where.is_not_null(field)
210 | return self
211 |
212 | def exists(self, query):
213 | self._where.exists(query)
214 | return self
215 |
216 | def not_exists(self, query):
217 | self._where.not_exists(query)
218 | return self
219 |
220 | def order_by(self, column, direction=''):
221 | self._order_by.append((column, direction))
222 | return self
223 |
224 | def limit(self, limt):
225 | self._limit = limt
226 | return self
227 |
228 | def compile_condition(self, condition):
229 | condition.compile(self.dialect, self)
230 | sql = condition.to_sql()
231 | self.bind.extend(condition.bind)
232 | return sql
233 |
234 | def compile_set(self, values):
235 | sets = []
236 | for column, value in values:
237 | column = self.dialect.quote_column(column)
238 | self.bind.append(value)
239 | sets.append(column + ' = ' + '%s')
240 |
241 | return ', '.join(sets)
242 |
243 | def compile_group_by(self, columns):
244 | group = []
245 | for column in columns:
246 | column = self.dialect.quote_identifier(column) if isinstance(column, list) else \
247 | self.dialect.quote_column(column)
248 | group.append(column)
249 |
250 | return 'GROUP BY ' + ', '.join(group)
251 |
252 | def compile_order_by(self, columns, direction=''):
253 | sorts = []
254 | for column, direction in columns:
255 | column = self.dialect.quote_identifier(column) if isinstance(column, list) else \
256 | self.dialect.quote_column(column)
257 | if direction:
258 | direction = ' ' + direction.upper()
259 | sorts.append(column + direction)
260 | return 'ORDER BY ' + ', '.join(sorts)
261 |
262 |
263 | def clear(self):
264 | self._where = QueryCondition('AND')
265 | self._order_by = []
266 | self._limit = None
267 | self.bind = []
268 |
269 |
270 |
271 | class SelectQuery(WhereQuery):
272 |
273 | def __init__(self, table, dialect, db, columns=None):
274 |
275 | self._select = []
276 | self._distinct = False
277 |
278 | self._from = [table]
279 | self._group_by = []
280 | self._offset = []
281 |
282 | self._select = columns or []
283 | self._db = db
284 | WhereQuery.__init__(self, dialect)
285 |
286 | def execute(self, many=None, as_dict=False):
287 | return self._db.query(self.to_sql(), self.bind, many=many, as_dict=as_dict)
288 |
289 | def distinct(self, distinct=True):
290 | self._distinct = distinct
291 | return self
292 |
293 | def select(self, *columns):
294 | self._select.extend(columns)
295 | return self
296 |
297 | fields = select
298 |
299 | def fram(self, tables):
300 | self._from.extend(tables)
301 | return self
302 |
303 | def group_by(self, *columns):
304 | self._group_by.extend(columns)
305 | return self
306 |
307 | def offset(self, offset):
308 | self._offset = offset
309 | return self
310 |
311 | def compile(self):
312 | quote_column = self.dialect.quote_column
313 | quote_table = self.dialect.quote_table
314 |
315 | sql = 'SELECT '
316 | if self._distinct:
317 | sql += 'DISTINCT '
318 | if not self._select:
319 | sql += '*'
320 | else:
321 | sql += ', '.join([quote_column(_) for _ in self._select])
322 |
323 | if self._from:
324 | sql += ' FROM ' + ', '.join([quote_table(_) for _ in self._from])
325 |
326 | if self._where:
327 | sql += '\nWHERE ' + self.compile_condition(self._where)
328 |
329 | if self._group_by:
330 | sql += '\n' + self.compile_group_by(self._group_by)
331 |
332 | if self._order_by:
333 | sql += '\n' + self.compile_order_by(self._order_by)
334 | if self._limit:
335 | sql += '\nLIMIT ' + str(self._limit)
336 |
337 | if self._offset:
338 | sql += ' OFFSET ' + str(self._offset)
339 | return sql
340 |
341 | def clear(self):
342 | self._select = []
343 | self._distinct = False
344 | self._from = []
345 | self._join = []
346 | self._group_by = []
347 | self._offset = []
348 | self._where = QueryCondition('AND')
349 | self._last_join = None
350 | self._limit = None
351 | self._offset = None
--------------------------------------------------------------------------------
/tests/dbt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import unittest
17 | import db
18 | import logging
19 |
20 |
21 |
22 | global config
23 |
24 | config = {
25 | 'passwd': 'test',
26 | 'user': 'test',
27 | 'host': 'localhost',
28 | 'db': 'test'
29 | }
30 |
31 |
32 | def _create():
33 | db.execute('DROP TABLE IF EXISTS `users`')
34 | db.execute("""CREATE TABLE `users` (
35 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
36 | `name` varchar(20),
37 | PRIMARY KEY (`uid`))""")
38 |
39 | class TestDBBase(unittest.TestCase):
40 |
41 | def setUp(self):
42 | setattr(db, '__db', {})
43 |
44 | def test_dup_key(self):
45 | db.setup(config)
46 | f = lambda: db.setup(config)
47 | self.assertRaises(db.DBError, f)
48 |
49 | def test_invalid_key(self):
50 | f = lambda: db.setup(config, key='dd.xx')
51 |
52 | self.assertRaises(TypeError, f)
53 |
54 |
55 | def test_database(self):
56 | db.setup(config)
57 | self.assertEqual(db.database(), db.database('default', slave=True))
58 | conns = getattr(db, '__db', [])
59 | self.assertEqual(len(conns['default.slave']), 1)
60 |
61 | db.setup(config, slave=True)
62 | self.assertNotEqual(db.database(), db.database('default', slave=True))
63 | conns = getattr(db, '__db', [])
64 | self.assertEqual(len(conns['default.slave']), 1)
65 |
66 | db.setup(config, slave=True)
67 | conns = getattr(db, '__db', [])
68 | self.assertEqual(len(conns['default.slave']), 2)
69 |
70 | class TestBase(unittest.TestCase):
71 |
72 | def setUp(self):
73 | setattr(db, '__db', {})
74 | db.setup(config)
75 | db.execute('DROP TABLE IF EXISTS `users`')
76 | db.execute("""CREATE TABLE `users` (
77 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
78 | `name` varchar(20) NOT NULL,
79 | PRIMARY KEY (`uid`))""")
80 |
81 | def test_query(self):
82 | self.assertEqual(1, db.query('SELECT 1')[0][0])
83 | self.assertEqual(0, len(db.query('SELECT * FROM users')))
84 |
85 | def test_execute(self):
86 | res = db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')])
87 | self.assertTrue(res)
88 | res = db.execute('DELETE FROM users WHERE name=%s', ('execute_test',))
89 | self.assertEqual(res, 2)
90 |
91 | def test_pool(self):
92 | import threading
93 |
94 | def q(n):
95 | for i in range(10):
96 | res = db.query('select count(*) from users')
97 | self.assertEqual(0, res[0][0])
98 | n = 50
99 | ts = []
100 | for i in range(n):
101 | t = threading.Thread(target=q, args=(i,))
102 | ts.append(t)
103 | for t in ts:
104 | t.start()
105 | for t in ts:
106 | t.join()
107 |
108 |
109 |
110 | class TestMultilDB(unittest.TestCase):
111 |
112 | def setUp(self):
113 | setattr(db, '__db', {})
114 | db.setup(config, key='test')
115 | db.setup(config, key='test', slave=True)
116 | db.execute('DROP TABLE IF EXISTS `users`', key='test')
117 | db.execute("""CREATE TABLE `users` (
118 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
119 | `name` varchar(20) NOT NULL,
120 | PRIMARY KEY (`uid`))""", key='test')
121 | rows = []
122 | for _ in range(1, 10):
123 | rows.append('(%d , "name_%d")' % (_, _))
124 | db.execute('INSERT INTO users VALUES ' + ', '.join(rows), key='test')
125 |
126 | def tearDown(self):
127 | db.execute('DELETE FROM users', key='test')
128 |
129 |
130 | def test_excute(self):
131 | res = db.execute('insert into users values(%s, %s)', [(10L, 'thomas'), (11L, 'animer')], key='test')
132 | res = db.query('SELECT count(*) FROM users WHERE uid>=10', key='test')
133 | self.assertEqual(2, res[0][0])
134 |
135 | def test_query(self):
136 | res = db.query('select name from users limit 5', key='test')
137 | self.assertEqual(len(res), 5)
138 | res = db.query('select name from users limit %s', (100,), many=20, key='test')
139 | rows = []
140 | for r in res:
141 | rows.append(r)
142 | self.assertTrue(10, len(rows))
143 |
144 | class TestSelectQuery(unittest.TestCase):
145 |
146 | def setUp(self):
147 | setattr(db, '__db', {})
148 | db.setup(config)
149 | _create()
150 | users = []
151 | for i in range(1, 5):
152 | users.append((i, 'user_' + str(i)))
153 | users.append((5, None))
154 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
155 | self.select = db.select('users')
156 |
157 | def tearDown(self):
158 | db.execute('delete from users')
159 |
160 |
161 | def test_select_all(self):
162 | self.assertEquals(len(self.select
163 | .execute()), 5)
164 |
165 |
166 | def test_select_as_dict(self):
167 | res = self.select.condition('uid', 1).execute(as_dict=True)
168 | self.assertEqual(len(res), 1)
169 | self.assertEqual(type(res[0]), dict)
170 | self.assertEqual(res[0]['uid'], 1)
171 |
172 | def test_select_many(self):
173 | res = (self.select.fields('*')
174 | .execute(many=2))
175 | rows = []
176 | for row in res:
177 | rows.append(row)
178 |
179 | self.assertEquals(len(rows), 5)
180 |
181 | def test_select_condition(self):
182 | res = (self.select
183 | .condition('name', 'user_1')
184 | .condition('uid', 1)
185 | .execute())
186 |
187 | self.assertEquals(res[0][1], 'user_1')
188 |
189 | def test_select_or_condition(self):
190 | from db import or_
191 | or_con = or_()
192 | or_con.condition('name', 'user_1')
193 | or_con.condition('name', 'user_2')
194 | res = (self.select
195 | .condition(or_con)
196 | .execute())
197 |
198 | self.assertEquals(res[0][1], 'user_1')
199 |
200 | def test_select_like(self):
201 | res = (self.select
202 | .condition('name', 'user_%', 'like')
203 | .execute())
204 | self.assertEquals(len(res), 4)
205 |
206 | def test_select_in(self):
207 | res = (self.select.fields('*')
208 | .condition('name', ['user_1', 'user_2'])
209 | .execute())
210 | self.assertEquals(res[0][1], 'user_1')
211 | self.assertEquals(res[1][1], 'user_2')
212 |
213 | def test_select_group_by(self):
214 | self.assertEquals(len(self.select
215 | .group_by('name', 'uid')
216 | .execute()), 5)
217 |
218 | def test_select_order_by_ASC(self):
219 |
220 | self.assertEquals(len(self.select
221 | .order_by('name')
222 | .execute()), 5)
223 |
224 | def test_select_order_by_DESC(self):
225 |
226 | self.assertEquals(len(self.select
227 | .order_by('name', 'DESC')
228 | .execute()), 5)
229 |
230 | def test_select_limit(self):
231 | self.assertEquals(len(self.select.limit(2).execute()), 2)
232 |
233 | def test_table_dot_condition(self):
234 | res = self.select.condition('users.uid', 5).execute()
235 | self.assertEqual(res[0][0], 5)
236 |
237 | def test_is_null(self):
238 | res = self.select.is_null('name').condition('uid', 5).execute()
239 | self.assertEqual(res[0][0], 5)
240 |
241 | def test_is_not_null(self):
242 | self.assertEqual(len(self.select.is_not_null('uid').execute()), 5)
243 |
244 | def test_expr(self):
245 | from db import expr
246 | res = self.select.fields(expr('count(*)')).execute()
247 | self.assertEqual(res[0][0], 5)
248 | res = db.select('users').fields(expr('count(uid)', 'total')).execute()
249 | self.assertEqual(res[0][0], 5)
250 |
251 |
252 | class TestUpdateQuery(unittest.TestCase):
253 |
254 | def setUp(self):
255 | setattr(db, '__db', {})
256 | db.setup(config)
257 | _create()
258 | users = []
259 | for i in range(1, 6):
260 | users.append((i, 'user_' + str(i)))
261 | db.execute('delete from users')
262 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
263 | self.update = db.update('users')
264 |
265 | def tearDown(self):
266 | db.execute('delete from users')
267 |
268 | def test_update_on_name(self):
269 | res = (self.update.
270 | mset({'name':'update_test'})
271 | .condition('name','user_1')
272 | .execute())
273 | self.assertEquals(res, 1)
274 |
275 |
276 | def test_update_on_name_and_uid(self):
277 | res = (self.update.
278 | set('name', 'update_test')
279 | .condition('name', 'user_2')
280 | .condition('uid', 2)
281 | .execute())
282 | self.assertEquals(res, 1)
283 |
284 | def test_update_not_exists(self):
285 | res = (self.update.
286 | mset({'name':'update', 'uid': 10})
287 | .condition('name', 'not_exists')
288 | .execute())
289 | self.assertEquals(res, 0)
290 |
291 | class TestInsertQuery(unittest.TestCase):
292 |
293 | def setUp(self):
294 | setattr(db, '__db', {})
295 | db.setup(config)
296 | _create()
297 | users = []
298 | for i in range(1, 6):
299 | users.append((i, 'user_' + str(i)))
300 | db.execute('delete from users')
301 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
302 | self.insert = db.insert('users')
303 | self.select = db.select('users')
304 |
305 | def tearDown(self):
306 | db.execute('delete from users')
307 |
308 | def test_insert(self):
309 | res = self.insert.values((10, 'test_insert')).execute()
310 | res = self.select.condition('name', 'test_insert').execute()
311 | self.assertEqual(res[0][1], 'test_insert')
312 |
313 | def test_insert_dict_values(self):
314 | self.insert.fields('name').values({'name': 'insert_1'}).values(('insert_2',)).execute()
315 | res = self.select.condition('name', ['insert_1', 'insert_2']).execute()
316 | self.assertEqual(len(res), 2)
317 |
318 | class TestDeleteQuery(unittest.TestCase):
319 |
320 | def setUp(self):
321 | setattr(db, '__db', {})
322 | db.setup(config)
323 | _create()
324 | users = []
325 | for i in range(1, 6):
326 | users.append((i, 'user_' + str(i)))
327 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
328 | self.delete = db.delete('users')
329 |
330 | def tearDown(self):
331 | db.execute('delete from users')
332 |
333 | def test_delete_by_uid(self):
334 | res = self.delete.condition('uid', 1).execute()
335 | self.assertEqual(res, 1)
336 |
337 | def test_delete_by_condtions(self):
338 | res = self.delete.condition('uid', 2).condition('name', 'user_2').execute()
339 | self.assertEqual(res, 1)
340 |
341 | def test_delete_or_condtions(self):
342 | from db import or_
343 | or_con = or_().condition('name', 'user_1').condition('name', 'user_2')
344 | res = self.delete.condition(or_con).execute()
345 | self.assertEqual(res, 2)
346 |
347 |
348 | class TestTransaction(unittest.TestCase):
349 |
350 | def setUp(self):
351 | setattr(db, '__db', {})
352 | db.setup(config)
353 | _create()
354 | users = []
355 | for i in range(1, 6):
356 | users.append((i, 'user_' + str(i)))
357 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
358 |
359 |
360 | def tearDown(self):
361 | db.execute('delete from users')
362 |
363 | def test_with(self):
364 | with db.transaction() as t:
365 | t.delete('users').condition('uid', 1).execute()
366 | res = db.select('users').condition('uid', 1).execute()
367 | self.assertEqual(len(res), 1)
368 | res = db.select('users').condition('uid', 1).execute()
369 | self.assertEqual(len(res), 0)
370 |
371 | def test_begin_commit(self):
372 | t = db.transaction()
373 | t.begin()
374 | t.delete('users').condition('uid', 1).execute()
375 | res = db.select('users').condition('uid', 1).execute()
376 | self.assertEqual(len(res), 1)
377 | t.commit()
378 | res = db.select('users').condition('uid', 1).execute()
379 | self.assertEqual(len(res), 0)
380 |
381 | if __name__ == '__main__':
382 | debug = True
383 | level = logging.DEBUG if debug else logging.INFO
384 | logging.basicConfig(level=level,
385 | format='%(asctime)s %(levelname)-8s %(message)s',
386 | datefmt='%Y-%m-%d %H:%M:%S', filemode='a+')
387 | unittest.main(verbosity=2 if debug else 0)
--------------------------------------------------------------------------------
/tests/pymysqlt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2014-2015 Thomas Huang
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, version 2 of the License.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import unittest
17 | import db
18 | import logging
19 |
20 |
21 |
22 | global config
23 |
24 | config = {
25 | 'passwd': 'test',
26 | 'user': 'test',
27 | 'host': 'localhost',
28 | 'db': 'test'
29 | }
30 |
31 |
32 | def _create():
33 | db.execute('DROP TABLE IF EXISTS `users`')
34 | db.execute("""CREATE TABLE `users` (
35 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
36 | `name` varchar(20),
37 | PRIMARY KEY (`uid`))""")
38 |
39 | class TestDBBase(unittest.TestCase):
40 |
41 | def setUp(self):
42 | setattr(db, '__db', {})
43 |
44 | def test_dup_key(self):
45 | db.setup(config,adapter='pymysql')
46 | f = lambda: db.setup(config,adapter='pymysql')
47 | self.assertRaises(db.DBError, f)
48 |
49 | def test_invalid_key(self):
50 | f = lambda: db.setup(config, key='dd.xx')
51 |
52 | self.assertRaises(TypeError, f)
53 |
54 |
55 | def test_database(self):
56 | db.setup(config,adapter='pymysql')
57 | self.assertEqual(db.database(), db.database('default', slave=True))
58 | conns = getattr(db, '__db', [])
59 | self.assertEqual(len(conns['default.slave']), 1)
60 |
61 | db.setup(config, slave=True)
62 | self.assertNotEqual(db.database(), db.database('default', slave=True))
63 | conns = getattr(db, '__db', [])
64 | self.assertEqual(len(conns['default.slave']), 1)
65 |
66 | db.setup(config, slave=True)
67 | conns = getattr(db, '__db', [])
68 | self.assertEqual(len(conns['default.slave']), 2)
69 |
70 | class TestBase(unittest.TestCase):
71 |
72 | def setUp(self):
73 | setattr(db, '__db', {})
74 | db.setup(config,adapter='pymysql')
75 | db.execute('DROP TABLE IF EXISTS `users`')
76 | db.execute("""CREATE TABLE `users` (
77 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
78 | `name` varchar(20) NOT NULL,
79 | PRIMARY KEY (`uid`))""")
80 |
81 | def test_query(self):
82 | self.assertEqual(1, db.query('SELECT 1')[0][0])
83 | self.assertEqual(0, len(db.query('SELECT * FROM users')))
84 |
85 | def test_execute(self):
86 | res = db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')])
87 | self.assertTrue(res)
88 | res = db.execute('DELETE FROM users WHERE name=%s', ('execute_test',))
89 | self.assertEqual(res, 2)
90 |
91 | def test_pool(self):
92 | import threading
93 |
94 | def q(n):
95 | for i in range(10):
96 | res = db.query('select count(*) from users')
97 | self.assertEqual(0, res[0][0])
98 | n = 50
99 | ts = []
100 | for i in range(n):
101 | t = threading.Thread(target=q, args=(i,))
102 | ts.append(t)
103 | for t in ts:
104 | t.start()
105 | for t in ts:
106 | t.join()
107 |
108 |
109 |
110 | class TestMultilDB(unittest.TestCase):
111 |
112 | def setUp(self):
113 | setattr(db, '__db', {})
114 | db.setup(config, key='test')
115 | db.setup(config, key='test', slave=True)
116 | db.execute('DROP TABLE IF EXISTS `users`', key='test')
117 | db.execute("""CREATE TABLE `users` (
118 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
119 | `name` varchar(20) NOT NULL,
120 | PRIMARY KEY (`uid`))""", key='test')
121 | rows = []
122 | for _ in range(1, 10):
123 | rows.append('(%d , "name_%d")' % (_, _))
124 | db.execute('INSERT INTO users VALUES ' + ', '.join(rows), key='test')
125 |
126 | def tearDown(self):
127 | db.execute('DELETE FROM users', key='test')
128 |
129 |
130 | def test_excute(self):
131 | res = db.execute('insert into users values(%s, %s)', [(10L, 'thomas'), (11L, 'animer')], key='test')
132 | res = db.query('SELECT count(*) FROM users WHERE uid>=10', key='test')
133 | self.assertEqual(2, res[0][0])
134 |
135 | def test_query(self):
136 | res = db.query('select name from users limit 5', key='test')
137 | self.assertEqual(len(res), 5)
138 | res = db.query('select name from users limit %s', (100,), many=20, key='test')
139 | rows = []
140 | for r in res:
141 | rows.append(r)
142 | self.assertTrue(10, len(rows))
143 |
144 | class TestSelectQuery(unittest.TestCase):
145 |
146 | def setUp(self):
147 | setattr(db, '__db', {})
148 | db.setup(config,adapter='pymysql')
149 | _create()
150 | users = []
151 | for i in range(1, 5):
152 | users.append((i, 'user_' + str(i)))
153 | users.append((5, None))
154 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
155 | self.select = db.select('users')
156 |
157 | def tearDown(self):
158 | db.execute('delete from users')
159 |
160 |
161 | def test_select_all(self):
162 | self.assertEquals(len(self.select
163 | .execute()), 5)
164 |
165 |
166 | def test_select_as_dict(self):
167 | res = self.select.condition('uid', 1).execute(as_dict=True)
168 | self.assertEqual(len(res), 1)
169 | self.assertEqual(type(res[0]), dict)
170 | self.assertEqual(res[0]['uid'], 1)
171 |
172 | def test_select_many(self):
173 | res = (self.select.fields('*')
174 | .execute(many=2))
175 | rows = []
176 | for row in res:
177 | rows.append(row)
178 |
179 | self.assertEquals(len(rows), 5)
180 |
181 | def test_select_condition(self):
182 | res = (self.select
183 | .condition('name', 'user_1')
184 | .condition('uid', 1)
185 | .execute())
186 |
187 | self.assertEquals(res[0][1], 'user_1')
188 |
189 | def test_select_or_condition(self):
190 | from db import or_
191 | or_con = or_()
192 | or_con.condition('name', 'user_1')
193 | or_con.condition('name', 'user_2')
194 | res = (self.select
195 | .condition(or_con)
196 | .execute())
197 |
198 | self.assertEquals(res[0][1], 'user_1')
199 |
200 | def test_select_like(self):
201 | res = (self.select
202 | .condition('name', 'user_%', 'like')
203 | .execute())
204 | self.assertEquals(len(res), 4)
205 |
206 | def test_select_in(self):
207 | res = (self.select.fields('*')
208 | .condition('name', ['user_1', 'user_2'])
209 | .execute())
210 | self.assertEquals(res[0][1], 'user_1')
211 | self.assertEquals(res[1][1], 'user_2')
212 |
213 | def test_select_group_by(self):
214 | self.assertEquals(len(self.select
215 | .group_by('name', 'uid')
216 | .execute()), 5)
217 |
218 | def test_select_order_by_ASC(self):
219 |
220 | self.assertEquals(len(self.select
221 | .order_by('name')
222 | .execute()), 5)
223 |
224 | def test_select_order_by_DESC(self):
225 |
226 | self.assertEquals(len(self.select
227 | .order_by('name', 'DESC')
228 | .execute()), 5)
229 |
230 | def test_select_limit(self):
231 | self.assertEquals(len(self.select.limit(2).execute()), 2)
232 |
233 | def test_table_dot_condition(self):
234 | res = self.select.condition('users.uid', 5).execute()
235 | self.assertEqual(res[0][0], 5)
236 |
237 | def test_is_null(self):
238 | res = self.select.is_null('name').condition('uid', 5).execute()
239 | self.assertEqual(res[0][0], 5)
240 |
241 | def test_is_not_null(self):
242 | self.assertEqual(len(self.select.is_not_null('uid').execute()), 5)
243 |
244 | def test_expr(self):
245 | from db import expr
246 | res = self.select.fields(expr('count(*)')).execute()
247 | self.assertEqual(res[0][0], 5)
248 | res = db.select('users').fields(expr('count(uid)', 'total')).execute()
249 | self.assertEqual(res[0][0], 5)
250 |
251 |
252 | class TestUpdateQuery(unittest.TestCase):
253 |
254 | def setUp(self):
255 | setattr(db, '__db', {})
256 | db.setup(config,adapter='pymysql')
257 | _create()
258 | users = []
259 | for i in range(1, 6):
260 | users.append((i, 'user_' + str(i)))
261 | db.execute('delete from users')
262 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
263 | self.update = db.update('users')
264 |
265 | def tearDown(self):
266 | db.execute('delete from users')
267 |
268 | def test_update_on_name(self):
269 | res = (self.update.
270 | mset({'name':'update_test'})
271 | .condition('name','user_1')
272 | .execute())
273 | self.assertEquals(res, 1)
274 |
275 |
276 | def test_update_on_name_and_uid(self):
277 | res = (self.update.
278 | set('name', 'update_test')
279 | .condition('name', 'user_2')
280 | .condition('uid', 2)
281 | .execute())
282 | self.assertEquals(res, 1)
283 |
284 | def test_update_not_exists(self):
285 | res = (self.update.
286 | mset({'name':'update', 'uid': 10})
287 | .condition('name', 'not_exists')
288 | .execute())
289 | self.assertEquals(res, 0)
290 |
291 | class TestInsertQuery(unittest.TestCase):
292 |
293 | def setUp(self):
294 | setattr(db, '__db', {})
295 | db.setup(config,adapter='pymysql')
296 | _create()
297 | users = []
298 | for i in range(1, 6):
299 | users.append((i, 'user_' + str(i)))
300 | db.execute('delete from users')
301 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
302 | self.insert = db.insert('users')
303 | self.select = db.select('users')
304 |
305 | def tearDown(self):
306 | db.execute('delete from users')
307 |
308 | def test_insert(self):
309 | res = self.insert.values((10, 'test_insert')).execute()
310 | res = self.select.condition('name', 'test_insert').execute()
311 | self.assertEqual(res[0][1], 'test_insert')
312 |
313 | def test_insert_dict_values(self):
314 | self.insert.fields('name').values({'name': 'insert_1'}).values(('insert_2',)).execute()
315 | res = self.select.condition('name', ['insert_1', 'insert_2']).execute()
316 | self.assertEqual(len(res), 2)
317 |
318 | class TestDeleteQuery(unittest.TestCase):
319 |
320 | def setUp(self):
321 | setattr(db, '__db', {})
322 | db.setup(config,adapter='pymysql')
323 | _create()
324 | users = []
325 | for i in range(1, 6):
326 | users.append((i, 'user_' + str(i)))
327 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
328 | self.delete = db.delete('users')
329 |
330 | def tearDown(self):
331 | db.execute('delete from users')
332 |
333 | def test_delete_by_uid(self):
334 | res = self.delete.condition('uid', 1).execute()
335 | self.assertEqual(res, 1)
336 |
337 | def test_delete_by_condtions(self):
338 | res = self.delete.condition('uid', 2).condition('name', 'user_2').execute()
339 | self.assertEqual(res, 1)
340 |
341 | def test_delete_or_condtions(self):
342 | from db import or_
343 | or_con = or_().condition('name', 'user_1').condition('name', 'user_2')
344 | res = self.delete.condition(or_con).execute()
345 | self.assertEqual(res, 2)
346 |
347 |
348 | class TestTransaction(unittest.TestCase):
349 |
350 | def setUp(self):
351 | setattr(db, '__db', {})
352 | db.setup(config,adapter='pymysql')
353 | _create()
354 | users = []
355 | for i in range(1, 6):
356 | users.append((i, 'user_' + str(i)))
357 | db.execute('INSERT INTO users VALUES(%s, %s)', users)
358 |
359 |
360 | def tearDown(self):
361 | db.execute('delete from users')
362 |
363 | def test_with(self):
364 | with db.transaction() as t:
365 | t.delete('users').condition('uid', 1).execute()
366 | res = db.select('users').condition('uid', 1).execute()
367 | self.assertEqual(len(res), 1)
368 | res = db.select('users').condition('uid', 1).execute()
369 | self.assertEqual(len(res), 0)
370 |
371 | def test_begin_commit(self):
372 | t = db.transaction()
373 | t.begin()
374 | t.delete('users').condition('uid', 1).execute()
375 | res = db.select('users').condition('uid', 1).execute()
376 | self.assertEqual(len(res), 1)
377 | t.commit()
378 | res = db.select('users').condition('uid', 1).execute()
379 | self.assertEqual(len(res), 0)
380 |
381 | if __name__ == '__main__':
382 | debug = True
383 | level = logging.DEBUG if debug else logging.INFO
384 | logging.basicConfig(level=level,
385 | format='%(asctime)s %(levelname)-8s %(message)s',
386 | datefmt='%Y-%m-%d %H:%M:%S', filemode='a+')
387 | unittest.main(verbosity=2 if debug else 0)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | le GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
--------------------------------------------------------------------------------
/README_CN.rst:
--------------------------------------------------------------------------------
1 | dbpy
2 | #####
3 |
4 |
5 | dbpy是一个python写的数据库CURD人性化api库。借鉴了 `webpy db `_ 和 `drupal database `_ 的设计。 如果喜欢 tornado db 或者 webpy db这类轻巧的db库,或者想发挥原生SQL优势,那么值得一试。
6 |
7 |
8 | Featues
9 | ================
10 |
11 | #. 灵活简单
12 | #. 天马行空的SQL构建语法糖
13 | #. 线程安全的连接池
14 | #. 支持读写分离(当前限定只能是一主多副模式)
15 | #. 支持简单事务
16 |
17 |
18 | The Projects use dbpy
19 | ======================
20 |
21 |
22 | `Lilac (Distributed Scheduler Task System) `_
23 |
24 | .. contents::
25 | :depth: 4
26 |
27 |
28 |
29 | Install
30 | ==============
31 |
32 |
33 |
34 | 使用easy_install安装::
35 |
36 | $ easy_install dbpy
37 |
38 | 或者使用pip下载安装::
39 |
40 |
41 | $ pip install dbpy
42 |
43 |
44 | 从github上fork下来,终端执行下面命令:
45 |
46 | .. code-block:: bash
47 |
48 | cd dbpy # the path to the project
49 | python setup.py install
50 |
51 | .. note:: 安装前先安装 ``MySQLdb`` (``MySQL-python``) 依赖python库
52 |
53 |
54 | Development
55 | ===========
56 |
57 | 下载后终端执行:
58 |
59 | .. code-block:: bash
60 |
61 | cd dbpy # the path to the project
62 | python setup.py develop
63 |
64 |
65 | Compatibility
66 | =============
67 |
68 | 在 Python 2.7+ 测试开发
69 |
70 | DB API
71 | ========
72 |
73 | 先提醒下模块使用单例模式。所以api相对比较好使。
74 |
75 |
76 | .. code-block:: python
77 |
78 | config = {
79 | 'passwd': 'test',
80 | 'user': 'test',
81 | 'host': 'localhost',
82 | 'db': 'test',
83 | 'max_idle' : 5*60
84 | }
85 |
86 | db.setup(config, minconn=5, maxconn=10,
87 | adapter='mysql', key='default', slave=False)
88 |
89 |
90 |
91 | setup
92 | ---------
93 |
94 | :config: 是数据库连接参数,可以传入MySQLDB#connect接口中所有的可选参数。 其中``max_idle`` 相对是mysql服务端 connect_timeout配置,默认10秒。
95 | :minconn: 为当前数据库连接池保持最小连接池,默认为5
96 | :maxconn: 为当前数据库连接池最大连接池,默认为10
97 | :adapter: 为适配器名,当前支持 mysql和pymsql
98 | :key: 是数据库的标识符,默认为 default
99 | :slave: 如果为true那么当前的数据库将会注册为读数据库。如果你没有做读写分离,只有一个数据库用来读写,那么setup一次就好,这样就可以读写。
100 |
101 | .. code-block:: python
102 |
103 | config = {
104 | 'passwd': 'test',
105 | 'user': 'test',
106 | 'host': 'localhost',
107 | 'db': 'test',
108 | 'max_idle' : 5*60
109 | }
110 |
111 | db.setup(config, key='test')
112 | config['host'] = 'test.slave'
113 | # 这次setup将会把key标记为仅可写,就是在后面用api时,制定到当前key的数据库会做数据分离
114 | db.setup(config, key='test', slave=True)
115 |
116 | config['host'] = 'test.slave2'
117 | # 再加入一个slave数据库
118 | db.setup(config, key='test', slave=True)
119 |
120 |
121 | config['host'] = 'host2'
122 | config['db'] = 'social'
123 | # 再加入一个数据库
124 | db.setup(config, key='social', slave=True)
125 |
126 | query
127 | -------
128 |
129 | query用于raw sql的查询语言。如果有更新数据请用execute.
130 |
131 | query(sql, args=None, many=None, as_dict=False, key='default'):
132 |
133 | :sql: mysql的格式化raw sql
134 | :args: 可以为元组和list,是sql格式化预处理的输入
135 | :many: 如果指定为大于零的整数将会使用fetchmany语句,并返回对象将会是迭代器.否则api调用fetchall返回结果.
136 | :as_dict: 如果为 true将会返回字典行,否则返回元组行。
137 | :key: 用于指定使用那个数据库。
138 |
139 |
140 | .. code-block:: python
141 |
142 | print db.query('SELECT 1')
143 | # > ((1L,),)
144 |
145 | # use social db
146 | print db.query('SELECT 1', key='social')
147 | # > ((1L,),)
148 |
149 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', (1, 'user_1'))
150 | # > ((1L, u'user_1'),)
151 |
152 | # Wanna return dict row
153 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s',
154 | (1, 'user_1'), as_dict=True)
155 | # > ({'uid': 1L, 'name': u'user_1'},)
156 |
157 | # Use fetchmany(many) then yeild, Return generator
158 | res = db.query('SELECT * FROM users WHERE uid=%s and name=%s',
159 | (1, 'user_1'), many=5, as_dict=True)
160 | print res
161 | print res.next()
162 | # >
163 | # > {'uid': 1L, 'name': u'user_1'}
164 |
165 |
166 | execute
167 | --------
168 |
169 | execute用于raw sql的更新语言。
170 | execute(sql, args=None, key='default'):
171 |
172 |
173 | :sql: mysql的格式化raw sql
174 | :args: 可以为元组和list,是sql格式化预处理的输入.如下面例子insert语句values有多个插入时,调用 ``executemany``
175 | :key: 用于指定使用那个数据库。
176 |
177 | 返回规范::
178 |
179 | 对于insert 将会返回 last_insert_id, 其他更新语句返回rowcount
180 |
181 | .. code-block:: python
182 |
183 | db.execute('DROP TABLE IF EXISTS `users`')
184 | db.execute("""CREATE TABLE `users` (
185 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
186 | `name` varchar(20) NOT NULL,
187 | PRIMARY KEY (`uid`))""")
188 |
189 | # insert语句插入多个value,注意这样写将会调用executemany,你懂的,就是封装了多条execute的玩意
190 | db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')])
191 | # > 9
192 | db.execute('DELETE FROM users WHERE name=%s', ('execute_test',))
193 | # > 2
194 |
195 |
196 | # use social db
197 | db.execute('delete from events where created_at<%s', (expired, ), key='social')
198 | # > 10
199 |
200 | select
201 | -----------
202 |
203 | select用于构建select 查询语言。
204 |
205 | select(table, key='default'):
206 |
207 | :table: 选定表
208 | :key: 用于指定使用那个数据库。
209 |
210 | select all
211 | ~~~~~~~~~~~~~~~~
212 |
213 | .. code-block:: python
214 |
215 | db.select('users')
216 | # > SELECT * FROM `users`
217 |
218 | specific columns
219 | ~~~~~~~~~~~~~~~~~
220 |
221 | .. code-block:: python
222 |
223 | db.select('users').fields('uid', 'name')
224 | # > SELECT `uid`, `name` FROM `users`
225 |
226 |
227 | execute
228 | ~~~~~~~~~~~~~~~~
229 |
230 | 在构建好查询条语句后使用execute api可以返回结果。
231 |
232 | execute(many=None, as_dict=False):
233 |
234 | :many: 如果指定为大于零的整数将会使用fetchmany语句,并返回对象将会是迭代器.否则api调用fetchall返回结果.
235 | :as_dict: 如果为 true将会返回字典行,否则返回元组行。
236 |
237 | .. code-block:: python
238 |
239 | q = db.select('users').fields('uid', 'name')
240 | res = q.execute()
241 | print res
242 | # > ((1L, u'user_1'), (2L, u'user_2'), (3L, u'user_3'), (4L, u'user_4'), (5L, None))
243 |
244 | res = q.execute(many=2, as_dict=True)
245 | print res
246 | print res.next()
247 | # >
248 | # > {'uid': 1L, 'name': u'user_1'}
249 |
250 |
251 | Condition
252 | ~~~~~~~~~~~
253 |
254 | 上面已经学会如何做简单的查询,那么如何组件条件查询。这里将会重点讲述condition方法如何构建各种查询条件。
255 |
256 | condition(field, value=None, operator=None):
257 |
258 | :field: 是条件限制的表字段
259 | :value: 是字段的条件值, 如果炸路额, operator都不指定就是 "field is null"
260 | :operator: 默认可能是等于操作符号, 可选的操作符号有 BETWEEN, IN, NOT IN, EXISTS, NOT EXISTS, IS NULL, IS NOT NULL, LIKE, NOT LIKE, =, <, >, >=, <=, <>等
261 |
262 |
263 | 在所有的select,update, delete查询中多个默认的condition将会是and条件组合。
264 |
265 | simple
266 | ^^^^^^^^^^^^^^^^
267 |
268 | .. code-block:: python
269 |
270 | db.select('users').condition('uid', 1) # condition('uid', 1, '=')
271 | # > SELECT * FROM `users`
272 | # > WHERE `uid` = %s
273 |
274 |
275 | in
276 | ^^^^^^^^^^^^^^^^
277 |
278 | .. code-block:: python
279 |
280 |
281 | db.select('users').condition('uid', (1, 3)) # condition('uid', [1, 3]) 一样
282 | # > SELECT * FROM `users`
283 | # > WHERE `uid` IN (%s, %s)
284 |
285 | between
286 | ^^^^^^^^^^^^^^^^
287 |
288 | .. code-block:: python
289 |
290 | db.select('users').condition('uid', (1, 3), 'between')
291 | # > SELECT * FROM `users`
292 | # > WHERE `uid` BETWEEN %s AND %s
293 |
294 |
295 | multi condition
296 | ^^^^^^^^^^^^^^^^^^^^^^^^
297 |
298 | .. code-block:: python
299 |
300 | db.select('users').condition('uid', 1).condition('name', 'blabla')
301 | # > SELECT * FROM `users`
302 | # > WHERE `uid` = %s AND `name` = %s
303 |
304 | or condition
305 | ^^^^^^^^^^^^^^
306 |
307 | .. code-block:: python
308 |
309 | or_cond = db.or_().condition('uid', 1).condition('name', 'blabla')
310 | db.select('users').condition(or_cond).condition('uid', 1, '<>')
311 | # > SELECT * FROM `users`
312 | # > WHERE ( `uid` = %s OR `name` = %s ) AND `uid` <> %s
313 |
314 |
315 |
316 | order by
317 | ~~~~~~~~~
318 |
319 | .. code-block:: python
320 |
321 | db.select('users').order_by('name')
322 | # > SELECT * FROM `users`
323 | # > ORDER BY `name`
324 |
325 | db.select('users').order_by('name', 'DESC')
326 | # > SELECT * FROM `users`
327 | # > ORDER BY `name` DESC
328 |
329 | db.select('users').order_by('name', 'DESC').order_by('uid')
330 | # > SELECT * FROM `users`
331 | # > ORDER BY `name` DESC, `uid`
332 |
333 |
334 |
335 | distinct
336 | ~~~~~~~~~
337 |
338 | .. code-block:: python
339 |
340 | db.select('users').distinct().condition('uid', 1)
341 | # > SELECT DISTINCT * FROM `users`
342 | # > WHERE `uid` = %s
343 |
344 | db.select('users').fields('uid', 'name').distinct().condition('uid', 1)
345 | # > SELECT DISTINCT `uid`, `name` FROM `users`
346 | # > WHERE `uid` = %s
347 |
348 |
349 | group by
350 | ~~~~~~~~~
351 |
352 | .. code-block:: python
353 |
354 | db.select('users').group_by('name', 'uid')
355 | # > SELECT * FROM `users`
356 | # > GROUP BY `name`, `uid`
357 |
358 |
359 | limit and offset
360 | ~~~~~~~~~~~~~~~~~
361 |
362 | .. code-block:: python
363 |
364 | db.select('users').limit(2).offset(5)
365 | # > SELECT * FROM `users`
366 | # > LIMIT 2 OFFSET 5
367 |
368 | null condition
369 | ~~~~~~~~~~~~~~~
370 |
371 | .. code-block:: python
372 |
373 | db.select('users').is_null('name').condition('uid', 5)
374 | # > SELECT * FROM `users`
375 | # > WHERE `name` IS NULL AND `uid` = %s
376 |
377 | db.select('users').is_not_null('name').condition('uid', 5)
378 | # > SELECT * FROM `users`
379 | # > WHERE `name` IS NOT NULL AND `uid` = %s
380 |
381 | db.select('users').condition('name', None)
382 | # > SELECT * FROM `users`
383 | # > WHERE `name` IS NULL
384 |
385 |
386 | complex conditions
387 | -------------------
388 |
389 | 使用 db.and_(), db.or_() 可以构建and或or粘合的条件组合。
390 |
391 | .. code-block:: python
392 |
393 | or_cond = db.or_().condition('field1', 1).condition('field2', 'blabla')
394 | and_cond = db.and_().condition('field3', 'what').condition('field4', 'then?')
395 | print db.select('table_name').condition(or_cond).condition(and_cond)
396 |
397 | # > SELECT * FROM `table_name`
398 | # > WHERE ( `field1` = %s OR `field2` = %s ) AND ( `field3` = %s AND `field4` = %s )
399 |
400 | expr
401 | ------------
402 |
403 | 如果你需要使用 count sum之类的集聚函数,那么使用 Expr构建字段吧。
404 |
405 | .. code-block:: python
406 |
407 | from db import expr
408 |
409 | db.select('users').fields(expr('count(*)'))
410 | # > SELECT count(*) FROM `users`
411 |
412 | db.select('users').fields(expr('count(uid)', 'total'))
413 | # > SELECT count(uid) AS `total` FROM `users`
414 |
415 |
416 |
417 | insert
418 | -----------
419 |
420 | insert用于构建insert into的sql语句。
421 |
422 | insert(table, key='default'):
423 |
424 | :table: 选定表
425 | :key: 用于指定使用那个数据库。
426 |
427 |
428 | .. code-block:: python
429 |
430 | q = db.insert('users').values((10, 'test_insert'))
431 | # > INSERT INTO `users` VALUES(%s, %s)
432 | print q._values
433 | # > [(10, 'test_insert')]
434 |
435 |
436 | q = db.insert('users').fields('name').values({'name': 'insert_1'}).values(('insert_2',))
437 | # > INSERT INTO `users` (`name`) VALUES(%s)
438 | print q._values
439 | # > [('insert_1',), ('insert_2',)]
440 |
441 | 构建好执行execute会执行数据库插入,execute返回的是last insert id:
442 |
443 | .. code-block:: python
444 |
445 |
446 | print q.execute()
447 | # > 2
448 |
449 |
450 |
451 | update
452 | -----------
453 |
454 | update用于构建update的sql语句
455 |
456 | update(table, key='default'):
457 |
458 | :table: 选定表
459 | :key: 用于指定使用那个数据库。
460 |
461 | update 主要可用的方法是mset和set, mset:
462 |
463 | :mset: 传入的是字典,用于一次set多个表属性
464 | :set(column, value): 只能设置一个属性,可以多次使用
465 |
466 | 构建条件codition前面已经讲述了。请参考 `select`_
467 |
468 |
469 | .. code-block:: python
470 |
471 |
472 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1')
473 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s
474 |
475 | q = (db.update('users').set('name', 'update_test').set('uid', 12)
476 | .condition('name', 'user_2').condition('uid', 2)) # .execute()
477 | print q.to_sql()
478 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s
479 |
480 |
481 | 构建好执行execute会执行数据库插入,execute返回的是更新的 rowcount:
482 |
483 | .. code-block:: python
484 |
485 |
486 | print q.execute()
487 | # > 2
488 |
489 | limit
490 | ~~~~~~~~~
491 |
492 | 因为你可能希望限制更新几条。那么可以使用limit
493 |
494 |
495 | .. code-block:: python
496 |
497 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1').limit(5)
498 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s LIMIT 5
499 |
500 | delete
501 | -----------
502 |
503 |
504 | delete 用于构建delete from的sql语句。
505 |
506 | delete(table, key='default'):
507 |
508 | :table: 选定表
509 | :key: 用于指定使用那个数据库。
510 |
511 | 构建条件codition前面已经讲述了。请参考 `select`_
512 |
513 | .. code-block:: python
514 |
515 | db.delete('users').condition('name','user_1')
516 | # > DELETE FROM `users` WHERE `name` = %s
517 |
518 | 构建好执行execute会执行数据库插入,execute返回的是删除的 rowcount:
519 |
520 | .. code-block:: python
521 |
522 |
523 | print q.execute()
524 | # > 2
525 |
526 |
527 | to_sql and str
528 | ---------------------
529 |
530 | ``db.insert``, ``db.update``, ``db.delete`` 返回的对象都可以使用 to_sql 或者__str__ 来查看构建成的sql语句。
531 |
532 |
533 | .. code-block:: python
534 |
535 |
536 | q = (db.update('users').set('name', 'update_test').set('uid', 12)
537 | .condition('name', 'user_2').condition('uid', 2))
538 | print q.to_sql()
539 | print q
540 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s
541 |
542 |
543 | transaction
544 | ------------
545 |
546 | transaction(table, key='default'):
547 |
548 | :table: 选定表
549 | :key: 用于指定使用那个数据库。
550 |
551 | 对于事务,这里比较简单的实现。要么全部执行,要么全部不做,没有做保存点。
552 |
553 |
554 |
555 | .. code-block:: python
556 |
557 |
558 | # with context
559 | with db.transaction() as t:
560 | t.delete('users').condition('uid', 1).execute()
561 | (t.update('users').mset({'name':None, 'uid' : 12})
562 | .condition('name','user_1').execute())
563 |
564 |
565 | # 普通用法
566 | t = db.transaction()
567 | t.begin()
568 | t.delete('users').condition('uid', 1).execute()
569 | (t.update('users').mset({'name':None, 'uid' : 12})
570 | .condition('name','user_1').execute())
571 |
572 | #这里将会提交,如果失败将会rollback
573 | t.commit()
574 |
575 | .. note:: 使用 begin一定要结合commit方法,不然可能连接不会返还连接池。建议用 ``with`` 语句。
576 |
577 |
578 | simple orm
579 | -----------
580 |
581 | 这里将会讲述最简单的orm构建技巧, 详细参考 `samples `_
582 |
583 | .. code-block:: python
584 |
585 | import model
586 | from orm import Backend
587 | import db
588 |
589 | db.setup({ 'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'})
590 |
591 |
592 | user = Backend('user').find_by_username('username')
593 | if user and user.check('password'):
594 | print 'auth'
595 |
596 | user = model.User('username', 'email', 'real_name', 'password',
597 | 'bio', 'status', 'role')
598 | if Backend('user').create(user):
599 | print 'fine'
600 |
601 | user = Backend('user').find(12)
602 | user.real_name = 'blablabla....'
603 | if Backend('user').save(user):
604 | print 'user saved'
605 |
606 | if Backend('user').delete(user):
607 | print 'delete user failed'
608 |
609 |
610 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js',
611 | 'category', 'status', 'comments', 'author')
612 | if not Backend('post').create(post):
613 | print 'created failed'
614 |
615 | Future
616 | --------
617 |
618 | 当前只支持mysql适配驱动,因为个人并不熟悉其他关联数据库,dbpy的设计比较灵活,所以如果有高手可以尝试写写其他数据库适配,仿照 `db/mysql目录 `_ 如果写pgsql的适配应该不会多余800行代码。
619 |
620 |
621 | 对于构建orm框架方面,从个人来讲,更喜欢原生SQL,也不打算再造一个orm轮子。从设计和实现来说,dbpy是为了更好的发挥原生SQL优势和简单灵活。
622 |
623 | 个人一些想法:
624 |
625 | #. 为select加入join构建方法糖。
626 | #. 尝试完成schema类,用于创建表,修改表结构等。
627 | #. 加入一些mysql特有的sql方法糖,比如replace, on dup更新等。
628 | #. 优化改进连接池,比如加入固定数量连接的连接池。
629 |
630 |
631 | LICENSE
632 | =======
633 |
634 | Copyright (C) 2014-2015 Thomas Huang
635 |
636 | This program is free software: you can redistribute it and/or modify
637 | it under the terms of the GNU General Public License as published by
638 | the Free Software Foundation, version 2 of the License.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU General Public License for more details.
644 |
645 | You should have received a copy of the GNU General Public License
646 | along with this program. If not, see .
647 |
648 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | dbpy
2 | #####
3 |
4 |
5 |
6 | dbpy is database abstration layer wrote by python. The design is inspired by `webpy db `_ and `drupal database `_ . If like the simple db abstration layer like ``tornado db`` or ``webpy db``, it is worth to try.
7 |
8 |
9 | `中文|chinese `_
10 |
11 | changes
12 | ==========
13 |
14 | #. Add pymysql adapter
15 |
16 |
17 |
18 | Install the extension with the following command::
19 |
20 | $ easy_install pymysql
21 |
22 | or alternatively if you have pip installed::
23 |
24 |
25 | $ pip install pymysql
26 |
27 | Featues
28 | ================
29 |
30 | #. silmple and flexible
31 | #. graceful and useful sql query builder.
32 | #. thread-safe connection pool
33 | #. supports read/write master-slave mode
34 | #. supports transaction
35 |
36 | The Projects use dbpy
37 | ======================
38 |
39 |
40 | `Lilac (Distributed Scheduler Task System) `_
41 |
42 | .. contents::
43 | :depth: 4
44 |
45 |
46 |
47 |
48 | Install
49 | ==============
50 |
51 | Install the extension with the following command::
52 |
53 | $ easy_install dbpy
54 |
55 | or alternatively if you have pip installed::
56 |
57 |
58 | $ pip install dbpy
59 |
60 |
61 | or clone it form github then run the command in shell:
62 |
63 | .. code-block:: bash
64 |
65 | cd db # the path to the project
66 | python setup.py install
67 |
68 | Development
69 | ===========
70 |
71 | Fork or download it, then run:
72 |
73 | .. code-block:: bash
74 |
75 | cd db # the path to the project
76 | python setup.py develop
77 |
78 |
79 |
80 | Compatibility
81 | =============
82 |
83 | Built and tested under Python 2.7+
84 |
85 |
86 | DB API
87 | ========
88 |
89 |
90 | Have a look:
91 |
92 | .. code-block:: python
93 |
94 | config = {
95 | 'passwd': 'test',
96 | 'user': 'test',
97 | 'host': 'localhost',
98 | 'db': 'test',
99 | 'max_idle' : 5*60
100 | }
101 |
102 | db.setup(config, minconn=5, maxconn=10,
103 | adapter='mysql', key='default', slave=False)
104 |
105 | db.execute('show tables')
106 |
107 |
108 |
109 | setup
110 | ---------
111 |
112 | :config: the connection basic config, the all of arguements of MySQLDB#connect is acceptable。 the ``max_idle`` is the connect timeout setting that is used to reconnection when connection is timeout, default is 10 seconds.
113 | :minconn: the minimum connections for the connection pool, default is 5.
114 | :maxconn: the maximum connections for the connection pool, default is 10.
115 | :adapter: the database driver adapter name, currently supports mysql (MySQLdb, pymysql) only.
116 | :key: the database idenfify for database, default database is "default"
117 | :slave: if set to true, the database will be register as a slave database. make sure you setup a master firstly.
118 |
119 |
120 | .. code-block:: python
121 |
122 | config = {
123 | 'passwd': 'test',
124 | 'user': 'test',
125 | 'host': 'localhost',
126 | 'db': 'test',
127 | 'max_idle' : 5*60
128 | }
129 |
130 | db.setup(config, key='test')
131 | config['host'] = 'test.slave'
132 | # set a slave, and now the master can only to write
133 | db.setup(config, key='test', slave=True)
134 |
135 | config['host'] = 'test.slave2'
136 | # add more slave for 'test'
137 | db.setup(config, key='test', slave=True)
138 |
139 |
140 | config['host'] = 'host2'
141 | config['db'] = 'social'
142 | # set another database
143 | db.setup(config, key='social', slave=True)
144 |
145 | query
146 | -------
147 |
148 |
149 |
150 | query api is used for reading database operation, like select..., show tables, if you wanna update your database please use execute api.
151 |
152 | query(sql, args=None, many=None, as_dict=False, key='default'):
153 |
154 | :sql: the raw sql
155 | :args: the args for sql arguement to prepare execute.
156 | :many: when set to a greater zero integer, it will use fetchmany then yield return a generator, otherwise a list.
157 | :as_dict: when set to true, query api will return the database result as dict row, otherwise tuple row.
158 | :key: the idenfify of database.
159 |
160 | .. code-block:: python
161 |
162 | print db.query('SELECT 1')
163 | # > ((1L,),)
164 |
165 | # use social db
166 | print db.query('SELECT 1', key='social')
167 | # > ((1L,),)
168 |
169 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s', (1, 'user_1'))
170 | # > ((1L, u'user_1'),)
171 |
172 | # Wanna return dict row
173 | print db.query('SELECT * FROM users WHERE uid=%s and name=%s',
174 | (1, 'user_1'), as_dict=True)
175 | # > ({'uid': 1L, 'name': u'user_1'},)
176 |
177 | # Use fetchmany(many) then yeild, Return generator
178 | res = db.query('SELECT * FROM users WHERE uid=%s and name=%s',
179 | (1, 'user_1'), many=5, as_dict=True)
180 | print res
181 | print res.next()
182 | # >
183 | # > {'uid': 1L, 'name': u'user_1'}
184 |
185 |
186 | execute
187 | --------
188 |
189 | the api is used for writing database operation, like insert, update, delete.. if you wanna read query your database please use query api.
190 |
191 | execute(sql, args=None, key='default'):
192 |
193 |
194 | :sql: the raw sql
195 | :args: the args for sql arguement to prepare execute.
196 | :key: the idenfify of database.
197 |
198 |
199 | Return::
200 |
201 | it returns last_insert_id when sql is insert statement, otherwise rowcount
202 |
203 | .. code-block:: python
204 |
205 | db.execute('DROP TABLE IF EXISTS `users`')
206 | db.execute("""CREATE TABLE `users` (
207 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
208 | `name` varchar(20) NOT NULL,
209 | PRIMARY KEY (`uid`))""")
210 |
211 | # when inset mutil-values,the api will call executemany
212 | db.execute('INSERT INTO users VALUES(%s, %s)', [(10, 'execute_test'), (9, 'execute_test')])
213 | # > 9
214 | db.execute('DELETE FROM users WHERE name=%s', ('execute_test',))
215 | # > 2
216 |
217 |
218 | # use social db
219 | db.execute('delete from events where created_at<%s', (expired, ), key='social')
220 | # > 10
221 |
222 | select
223 | -----------
224 |
225 | the api is used for select sql database query.
226 |
227 | select(table, key='default'):
228 |
229 | :table: the table name
230 | :key: the idenfify of database
231 |
232 | select all
233 | ~~~~~~~~~~~~~~~~
234 |
235 | .. code-block:: python
236 |
237 | db.select('users')
238 | # > SELECT * FROM `users`
239 |
240 | specific columns
241 | ~~~~~~~~~~~~~~~~~
242 |
243 | .. code-block:: python
244 |
245 | db.select('users').fields('uid', 'name')
246 | # > SELECT `uid`, `name` FROM `users`
247 |
248 |
249 | execute
250 | ~~~~~~~~~~~~~~~~
251 |
252 | when you already build your sql, try execute api to fetch your database result.
253 |
254 | execute(many=None, as_dict=False):
255 |
256 | :many: when set to a greater zero integer, it will use fetchmany then yield return a generator, otherwise a list.
257 | :as_dict: when set to true, query api will return the database result as dict row, otherwise tuple row.
258 |
259 | .. code-block:: python
260 |
261 | q = db.select('users').fields('uid', 'name')
262 | res = q.execute()
263 | print res
264 | # > ((1L, u'user_1'), (2L, u'user_2'), (3L, u'user_3'), (4L, u'user_4'), (5L, None))
265 |
266 | res = q.execute(many=2, as_dict=True)
267 | print res
268 | print res.next()
269 | # >
270 | # > {'uid': 1L, 'name': u'user_1'}
271 |
272 |
273 | Condition
274 | ~~~~~~~~~~~
275 |
276 | It is time to try more complex select query.
277 |
278 | condition(field, value=None, operator=None):
279 |
280 | :field: the field of table
281 | :value: the value of field, defaul is None ("field is null")
282 | :operator: the where operator like BETWEEN, IN, NOT IN, EXISTS, NOT EXISTS, IS NULL, IS NOT NULL, LIKE, NOT LIKE, =, <, >, >=, <=, <> and so on.
283 |
284 |
285 | simple
286 | ^^^^^^^^^^^^^^^^
287 |
288 | .. code-block:: python
289 |
290 | db.select('users').condition('uid', 1) # condition('uid', 1, '=')
291 | # > SELECT * FROM `users`
292 | # > WHERE `uid` = %s
293 |
294 |
295 | in
296 | ^^^^^^^^^^^^^^^^
297 |
298 | .. code-block:: python
299 |
300 |
301 | db.select('users').condition('uid', (1, 3)) # condition('uid', [1, 3]) 一样
302 | # > SELECT * FROM `users`
303 | # > WHERE `uid` IN (%s, %s)
304 |
305 | between
306 | ^^^^^^^^^^^^^^^^
307 |
308 | .. code-block:: python
309 |
310 | db.select('users').condition('uid', (1, 3), 'between')
311 | # > SELECT * FROM `users`
312 | # > WHERE `uid` BETWEEN %s AND %s
313 |
314 |
315 | multi condition
316 | ^^^^^^^^^^^^^^^^^^^^^^^^
317 |
318 | .. code-block:: python
319 |
320 | db.select('users').condition('uid', 1).condition('name', 'blabla')
321 | # > SELECT * FROM `users`
322 | # > WHERE `uid` = %s AND `name` = %s
323 |
324 | or condition
325 | ^^^^^^^^^^^^^^
326 |
327 | .. code-block:: python
328 |
329 | or_cond = db.or_().condition('uid', 1).condition('name', 'blabla')
330 | db.select('users').condition(or_cond).condition('uid', 1, '<>')
331 | # > SELECT * FROM `users`
332 | # > WHERE ( `uid` = %s OR `name` = %s ) AND `uid` <> %s
333 |
334 |
335 |
336 | order by
337 | ~~~~~~~~~
338 |
339 | .. code-block:: python
340 |
341 | db.select('users').order_by('name')
342 | # > SELECT * FROM `users`
343 | # > ORDER BY `name`
344 |
345 | db.select('users').order_by('name', 'DESC')
346 | # > SELECT * FROM `users`
347 | # > ORDER BY `name` DESC
348 |
349 | db.select('users').order_by('name', 'DESC').order_by('uid')
350 | # > SELECT * FROM `users`
351 | # > ORDER BY `name` DESC, `uid`
352 |
353 |
354 |
355 | distinct
356 | ~~~~~~~~~
357 |
358 | .. code-block:: python
359 |
360 | db.select('users').distinct().condition('uid', 1)
361 | # > SELECT DISTINCT * FROM `users`
362 | # > WHERE `uid` = %s
363 |
364 | db.select('users').fields('uid', 'name').distinct().condition('uid', 1)
365 | # > SELECT DISTINCT `uid`, `name` FROM `users`
366 | # > WHERE `uid` = %s
367 |
368 |
369 | group by
370 | ~~~~~~~~~
371 |
372 | .. code-block:: python
373 |
374 | db.select('users').group_by('name', 'uid')
375 | # > SELECT * FROM `users`
376 | # > GROUP BY `name`, `uid`
377 |
378 |
379 | limit and offset
380 | ~~~~~~~~~~~~~~~~~
381 |
382 | .. code-block:: python
383 |
384 | db.select('users').limit(2).offset(5)
385 | # > SELECT * FROM `users`
386 | # > LIMIT 2 OFFSET 5
387 |
388 | null condition
389 | ~~~~~~~~~~~~~~~
390 |
391 | .. code-block:: python
392 |
393 | db.select('users').is_null('name').condition('uid', 5)
394 | # > SELECT * FROM `users`
395 | # > WHERE `name` IS NULL AND `uid` = %s
396 |
397 | db.select('users').is_not_null('name').condition('uid', 5)
398 | # > SELECT * FROM `users`
399 | # > WHERE `name` IS NOT NULL AND `uid` = %s
400 |
401 | db.select('users').condition('name', None)
402 | # > SELECT * FROM `users`
403 | # > WHERE `name` IS NULL
404 |
405 |
406 | complex conditions
407 | -------------------
408 |
409 | using db.and_(), db.or_(), we can build complex where conditions:
410 |
411 | .. code-block:: python
412 |
413 | or_cond = db.or_().condition('field1', 1).condition('field2', 'blabla')
414 | and_cond = db.and_().condition('field3', 'what').condition('field4', 'then?')
415 | print db.select('table_name').condition(or_cond).condition(and_cond)
416 |
417 | # > SELECT * FROM `table_name`
418 | # > WHERE ( `field1` = %s OR `field2` = %s ) AND ( `field3` = %s AND `field4` = %s )
419 |
420 | expr
421 | ------------
422 |
423 | if you wanna use the aggregate functions like sum, count, please use ``erpr`` :
424 |
425 | .. code-block:: python
426 |
427 | from db import expr
428 |
429 | db.select('users').fields(expr('count(*)'))
430 | # > SELECT count(*) FROM `users`
431 |
432 | db.select('users').fields(expr('count(uid)', 'total'))
433 | # > SELECT count(uid) AS `total` FROM `users`
434 |
435 |
436 |
437 | insert
438 | -----------
439 |
440 | The ``insert`` api is used for building insert into sql statement.
441 |
442 | insert(table, key='default'):
443 |
444 | :table: the table name
445 | :key: the idenfify of database
446 |
447 | .. code-block:: python
448 |
449 | q = db.insert('users').values((10, 'test_insert'))
450 | # > INSERT INTO `users` VALUES(%s, %s)
451 | print q._values
452 | # > [(10, 'test_insert')]
453 |
454 |
455 | q = db.insert('users').fields('name').values({'name': 'insert_1'}).values(('insert_2',))
456 | # > INSERT INTO `users` (`name`) VALUES(%s)
457 | print q._values
458 | # > [('insert_1',), ('insert_2',)]
459 |
460 |
461 | When you use ``execute`` api to get result, it will reutrn the ``last insert id``:
462 |
463 | .. code-block:: python
464 |
465 |
466 | print q.execute()
467 | # > 2
468 |
469 |
470 |
471 | update
472 | -----------
473 |
474 | The ``update`` api is used for building update sql statement.
475 |
476 | update(table, key='default'):
477 |
478 | :table: the table name
479 | :key: the idenfify of database
480 |
481 |
482 | mset and set:
483 |
484 | :mset: the value must be dict tpye, that sets mutil-fileds at once time.
485 | :set(column, value): set one field one time.
486 |
487 | the where conditions please see `select`_ for more information.
488 |
489 |
490 | .. code-block:: python
491 |
492 |
493 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1')
494 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s
495 |
496 | q = (db.update('users').set('name', 'update_test').set('uid', 12)
497 | .condition('name', 'user_2').condition('uid', 2)) # .execute()
498 | print q.to_sql()
499 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s
500 |
501 |
502 |
503 | When you use ``execute`` api to get result, it will reutrn the ``rowcount``:
504 |
505 |
506 | .. code-block:: python
507 |
508 |
509 | print q.execute()
510 | # > 2
511 |
512 | limit
513 | ~~~~~~~~~
514 |
515 |
516 |
517 | You can use limit api to lim the quantity of update.
518 |
519 |
520 | .. code-block:: python
521 |
522 | db.update('users').mset({'name':None, 'uid' : 12}).condition('name','user_1').limit(5)
523 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s LIMIT 5
524 |
525 | delete
526 | -----------
527 |
528 |
529 | The ``delete`` api is used for building DELETE FROM sql statement.
530 |
531 | delete(table, key='default'):
532 |
533 | :table: the table name
534 | :key: the idenfify of database
535 |
536 | the where conditions please see `select`_ for more information.
537 |
538 | .. code-block:: python
539 |
540 | db.delete('users').condition('name','user_1')
541 | # > DELETE FROM `users` WHERE `name` = %s
542 |
543 | When you use ``execute`` api to get result, it will reutrn the ``rowcount``:
544 |
545 | .. code-block:: python
546 |
547 |
548 | print q.execute()
549 | # > 2
550 |
551 |
552 | to_sql and str
553 | ---------------------
554 |
555 | you can use to_sql or __str__ method to the objects of ``select``, ``insert``, ``update``, ``delete`` to print the sql you build.
556 |
557 |
558 | .. code-block:: python
559 |
560 |
561 | q = (db.update('users').set('name', 'update_test').set('uid', 12)
562 | .condition('name', 'user_2').condition('uid', 2))
563 | print q.to_sql()
564 | print q
565 | # > UPDATE `users` SET `name` = %s, `uid` = %s WHERE `name` = %s AND `uid` = %s
566 |
567 |
568 | transaction
569 | ------------
570 |
571 | transaction(table, key='default'):
572 |
573 | :table: the table name
574 | :key: the idenfify of database
575 |
576 |
577 | The simple transaction done all or do nothing, you cann't set savepoint.
578 |
579 |
580 |
581 | .. code-block:: python
582 |
583 |
584 | # with context
585 | with db.transaction() as t:
586 | t.delete('users').condition('uid', 1).execute()
587 | (t.update('users').mset({'name':None, 'uid' : 12})
588 | .condition('name','user_1').execute())
589 |
590 |
591 | # the normal way
592 | t = db.transaction()
593 | t.begin()
594 | t.delete('users').condition('uid', 1).execute()
595 | (t.update('users').mset({'name':None, 'uid' : 12})
596 | .condition('name','user_1').execute())
597 |
598 | #if failed will rollback
599 | t.commit()
600 |
601 | .. note:: when uses begin must be combine with commit,otherwise the connection will not return connection pool.suggets to use ``with context``
602 |
603 |
604 | simple orm
605 | -----------
606 |
607 | the orm demo `samples `_
608 |
609 | .. code-block:: python
610 |
611 | import model
612 | from orm import Backend
613 | import db
614 |
615 | db.setup({ 'host': 'localhost', 'user': 'test', 'passwd': 'test', 'db': 'blog'})
616 |
617 |
618 | user = Backend('user').find_by_username('username')
619 | if user and user.check('password'):
620 | print 'auth'
621 |
622 | user = model.User('username', 'email', 'real_name', 'password',
623 | 'bio', 'status', 'role')
624 | if Backend('user').create(user):
625 | print 'fine'
626 |
627 | user = Backend('user').find(12)
628 | user.real_name = 'blablabla....'
629 | if Backend('user').save(user):
630 | print 'user saved'
631 |
632 | if Backend('user').delete(user):
633 | print 'delete user failed'
634 |
635 |
636 | post = model.Post('title', 'slug', 'description', 'html', 'css', 'js',
637 | 'category', 'status', 'comments', 'author')
638 | if not Backend('post').create(post):
639 | print 'created failed'
640 |
641 | Future
642 | --------
643 |
644 |
645 | Personal idea:
646 |
647 | #. add ``join`` for select api
648 | #. add a schema class for creating or changing table.
649 | #. add some api for mysql individual sql like ``replace`` or ``duplicate update``
650 | #. improve connection pool.
651 |
652 |
653 | LICENSE
654 | =======
655 |
656 | Copyright (C) 2014-2015 Thomas Huang
657 |
658 | This program is free software: you can redistribute it and/or modify
659 | it under the terms of the GNU General Public License as published by
660 | the Free Software Foundation, version 2 of the License.
661 |
662 | This program is distributed in the hope that it will be useful,
663 | but WITHOUT ANY WARRANTY; without even the implied warranty of
664 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
665 | GNU General Public License for more details.
666 |
667 | You should have received a copy of the GNU General Public License
668 | along with this program. If not, see .
669 |
670 |
--------------------------------------------------------------------------------