Skip to content

Commit

Permalink
Rename query to statement
Browse files Browse the repository at this point in the history
  • Loading branch information
uranusjr committed Sep 8, 2017
1 parent 4c00c6d commit 08606f2
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 148 deletions.
16 changes: 8 additions & 8 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Quickstart
SQLian is composed of three main parts:

* **Connectors** connect to a database.
* **Queries** take native objects and convert them to a SQL command. The
command can then be executed by the connector in the database.
* **Statement builders** take native objects and convert them to a SQL command
string. The command can then be executed by the connector in the database.
* **Records** are wrappers providing a clean, nice interface to interact with
the database cursor, and the data it retrieves.

Expand Down Expand Up @@ -37,11 +37,11 @@ DB-API 2.0 specification (`PEP 249`_), so you can get to work directly if you
know your way around. But there's a better way to do it.


Making queries
---------------
Issuing commands
-----------------

Aside from the DB-API 2.0-compatible stuff, the `Connection` object also
provides a rich set of "query builders" that frees you from formatting SQL
provides a rich set of "statement builders" that frees you from formatting SQL
yourself, and convert native Python objects more easily for SQL usage.

Let's insert some data first:
Expand Down Expand Up @@ -95,14 +95,14 @@ You'd guess how deletion works by now, so let's add a little twist:
db.delete('person', where={'occupation !=': 'Pinkoi'})
The query build automatically parse trailing operators and do the right thing.
The builder automatically parse trailing operators and do the right thing.


Handling results
-----------------

Some queries produce data. For every query, SQLian returns an iterable object
so you can handle those data.
Some statements produce data. For every query, SQLian returns an iterable
object so you can handle those data.

.. code-block:: pycon
Expand Down
2 changes: 1 addition & 1 deletion sqlian/mysql/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class Engine(BaseEngine):

from . import clauses, queries
from . import clauses, statements

identifier_quote = '`'

Expand Down
8 changes: 4 additions & 4 deletions sqlian/mysql/queries.py → sqlian/mysql/statements.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Inject everything from standard SQL.
from sqlian.standard.queries import * # noqa
from sqlian.standard.statements import * # noqa

from sqlian.standard.queries import (
__all__, Query,
from sqlian.standard.statements import (
__all__, Statement,
Insert as StandardInsert,
Select as StandardSelect,
)
Expand All @@ -25,7 +25,7 @@ class Select(StandardSelect):
]


class Replace(Query):
class Replace(Statement):
sql_name = 'REPLACE'
param_classes = [
('replace', c.ReplaceInto),
Expand Down
2 changes: 1 addition & 1 deletion sqlian/postgresql/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class Engine(BaseEngine):

from . import clauses, queries
from . import clauses, statements

def escape_string(self, value):
if '\0' in value: # PostgreSQL doesn't handle NULL byte well?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Inject everything from standard SQL.
from sqlian.standard.queries import * # noqa
from sqlian.standard.statements import * # noqa

from sqlian.standard.queries import (
from sqlian.standard.statements import (
__all__,
Insert as StandardInsert,
Select as StandardSelect,
Expand Down
19 changes: 10 additions & 9 deletions sqlian/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,16 @@ def __bool__(self):
def __nonzero__(self):
return self.__bool__()

# TODO: Handle ProgrammerError.
# TODO: Handle non-query errors.
# DB-API states for `fetchone()`, "an Error (or subclass) exception is
# raised if the previous call to .execute*() did not produce any result
# set or no call was issued yet."
# This means we either (1) Need to "know" a query doesn't return a result,
# and don't build a cursor-based collection at all, or (2) Should catch
# this error here alongside with StopIteration. (1) should be the better
# approach because we can't really know if the error is caused by an empty
# execution result, but unfortunately it's also extremely difficult to
# know whether a query returns result (e.g. INSERT INTO usually doesn't,
# but can with RETURNING; SELECT usually does, but doesn't if you have an
# INTO clause). For now we just rely on the programmer to handle this.
# This means we either (1) Need to "know" a statement doesn't return a
# result (i.e. is not a query), and don't build a cursor-based collection
# at all, or (2) Should catch this error here alongside with StopIteration.
# (1) should be the better approach because we can't really know if the
# error is caused by an empty execution result, but unfortunately it's
# also extremely difficult to know upfront whether a statement is a query.
# For example, INSERT INTO usually is not a query, but can return results
# with RETURNING; SELECT usually is a query, but doesn't return results if
# you have an INTO clause). For now we rely on the user to handle this.
14 changes: 7 additions & 7 deletions sqlian/standard/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,20 @@ def cursor(self):

# Things!

def perform_query(self, name, args, kwargs):
query = getattr(self._engine, name)(*args, **kwargs)
def execute_statement(self, name, args, kwargs):
statement = getattr(self._engine, name)(*args, **kwargs)
cursor = self._conn.cursor()
cursor.execute(query)
cursor.execute(statement)
return RecordCollection.from_cursor(cursor)

def select(self, *args, **kwargs):
return self.perform_query('select', args, kwargs)
return self.execute_statement('select', args, kwargs)

def insert(self, *args, **kwargs):
return self.perform_query('insert', args, kwargs)
return self.execute_statement('insert', args, kwargs)

def update(self, *args, **kwargs):
return self.perform_query('update', args, kwargs)
return self.execute_statement('update', args, kwargs)

def delete(self, *args, **kwargs):
return self.perform_query('delete', args, kwargs)
return self.execute_statement('delete', args, kwargs)
48 changes: 24 additions & 24 deletions sqlian/standard/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ def ensure_sql_wrapped(func):
return ensure_sql(func)


def query_builder(f):
"""Convert decorated callable to a query builder.
def statement_builder(f):
"""Convert decorated callable to build a statement.
The decorated callable should return a 3-tuple of the query class,
the args, and kwargs to build to the query. This decorator parses the
arguments into appropriate clauses, and instantiate an instance of
query class with those clauses.
The decorated callable should return a 3-tuple, containing the statement
class's name, the args, and kwargs to build the statement. This decorator
parses the arguments into appropriate clauses, and instantiate a statement
instance with those clauses.
"""
@functools.wraps(f)
def wrapped(self, *args, **kwargs):
query_klass, args, kwargs = f(self, *args, **kwargs)
param_cls = {k: klass for k, klass in query_klass.param_classes}
klass_name, args, kwargs = f(self, *args, **kwargs)
statement_klass = getattr(self.statements, klass_name)
param_cls = {k: klass for k, klass in statement_klass.param_classes}
native_args, clause_args = map(
list, partition(lambda arg: isinstance(arg, Clause), args),
)
Expand All @@ -47,22 +48,21 @@ def wrapped(self, *args, **kwargs):

# Convert native arguments into an extra clause.
if native_args:
klass = query_klass.default_param_class
klass = statement_klass.default_param_class
prepend_args.append(
klass.parse(native_args[0], self) if len(native_args) == 1
else klass.parse(native_args, self)
)

# Convert kwargs into extra clauses.
for key, arg in kwargs.items():
if key in query_klass.param_aliases:
key = query_klass.param_aliases[key]
if key in statement_klass.param_aliases:
key = statement_klass.param_aliases[key]
prepend_args.append(param_cls[key].parse(arg, self))

query = query_klass(*(prepend_args + clause_args))
return query.__sql__(self)
statement = statement_klass(*(prepend_args + clause_args))
return statement.__sql__(self)

wrapped.__query_builder__ = True
return wrapped


Expand Down Expand Up @@ -123,11 +123,11 @@ class Engine(BaseEngine):
"""Engine that emits ANSI-compliant SQL.
"""
from . import (
clauses, compositions, constants, expressions, functions, queries,
clauses, compositions, constants, expressions, functions, statements,
)

# Perform "from X import *" for these modules.
# Clauses and queries are NOT loaded into the top scope because they
# Clauses and statements are NOT loaded into the top scope because they
# are always acceible by engine methods and their kwargs.
locals().update({k: v for k, v in iter_all_members(
compositions, constants, expressions, functions,
Expand Down Expand Up @@ -201,13 +201,13 @@ def as_identifier(self, name):

# Shorthand methods.

@query_builder
@statement_builder
def select(self, *args, **kwargs):
if not args and 'select' not in kwargs:
kwargs['select'] = self.star
return self.queries.Select, args, kwargs
return 'Select', args, kwargs

@query_builder
@statement_builder
def insert(self, *args, **kwargs):
# Unpack mapping 'values' kwarg into 'columns' and 'values' kwargs.
# This only happens if the 'columns' kwarg is not already set.
Expand All @@ -227,15 +227,15 @@ def insert(self, *args, **kwargs):
'columns': columns,
'values': [[d[k] for k in columns] for d in values_kwarg],
})
return self.queries.Insert, args, kwargs
return 'Insert', args, kwargs

@query_builder
@statement_builder
def update(self, *args, **kwargs):
return self.queries.Update, args, kwargs
return 'Update', args, kwargs

@query_builder
@statement_builder
def delete(self, *args, **kwargs):
return self.queries.Delete, args, kwargs
return 'Delete', args, kwargs

def join(self, join_item, on=None, using=None, join_type=''):
if on is not None and using is not None:
Expand Down
36 changes: 18 additions & 18 deletions sqlian/standard/queries.py → sqlian/standard/statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,34 @@


__all__ = [
'Query', 'QueryError', 'DuplicateClauseError', 'InvalidClauseError',
'Select', 'Insert', 'Update', 'Delete',
'StatementError', 'DuplicateClauseError', 'InvalidClauseError',
'Statement', 'Select', 'Insert', 'Update', 'Delete',
]


class QueryError(ValueError):
def __init__(self, clause_name, query_name):
super(QueryError, self).__init__(self.error_template.format(
clause=clause_name, query=query_name,
class StatementError(ValueError):
def __init__(self, clause_name, statement_name):
super(StatementError, self).__init__(self.error_template.format(
clause=clause_name, statement=statement_name,
))
self.clause = clause_name
self.query = query_name
self.clause_name = clause_name
self.statement_name = statement_name


class DuplicateClauseError(QueryError):
error_template = 'duplicate {clause} clauses for query {query}'
class DuplicateClauseError(StatementError):
error_template = 'duplicate {clause} clauses for statement {statement}'


class InvalidClauseError(QueryError):
error_template = 'Query {query} does not accept clause {clause}'
class InvalidClauseError(StatementError):
error_template = 'Statement {statement} does not accept clause {clause}'


class Query(object):
class Statement(object):

param_aliases = ()

def __init__(self, *args):
super(Query, self).__init__()
super(Statement, self).__init__()
self.param_clauses = self._map_clause_to_params(args)

def __repr__(self):
Expand Down Expand Up @@ -71,7 +71,7 @@ def _map_clause_to_params(self, clauses):
return param_clauses


class Select(Query):
class Select(Statement):
sql_name = 'SELECT'
param_classes = [
('select', c.Select),
Expand All @@ -91,7 +91,7 @@ class Select(Query):
}


class Insert(Query):
class Insert(Statement):
sql_name = 'INSERT'
param_classes = [
('insert', c.InsertInto),
Expand All @@ -101,7 +101,7 @@ class Insert(Query):
default_param_class = c.InsertInto


class Update(Query):
class Update(Statement):
sql_name = 'UPDATE'
param_classes = [
('update', c.Update),
Expand All @@ -112,7 +112,7 @@ class Update(Query):
param_aliases = {'set_': 'set'}


class Delete(Query):
class Delete(Statement):
sql_name = 'DELETE'
param_classes = [
('delete', c.DeleteFrom),
Expand Down
2 changes: 1 addition & 1 deletion tests/database/test_py_postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def database_name(request):
try:
conn = dbapi.connect(database='postgres')
except dbapi.OperationalError:
except dbapi.DatabaseError:
return None
conn.autocommit = True # Required for CREATE DATABASE.

Expand Down
4 changes: 2 additions & 2 deletions tests/mysql/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ def m(engine):


@pytest.fixture
def q(engine):
return engine.queries
def s(engine):
return engine.statements
Loading

0 comments on commit 08606f2

Please sign in to comment.