[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/7dracoder/python-spanner-orm/main/spanner_orm/query.py [Back]  [Original]

# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helps build SQL for complex Spanner queries."""

import abc
from typing import Any, Dict, Generic, Iterable, List, Sequence, Tuple, Type, TypeVar

from spanner_orm import condition
from spanner_orm import error

ResultType = TypeVar('ResultType')


class SpannerQuery(abc.ABC, Generic[ResultType]):
  """Helps build SQL for complex Spanner queries."""

  def __init__(self, model: Type[Any],
               conditions: Iterable[condition.Condition]):
    self.param_offset = 0
    self._model = model
    self._conditions = conditions
    self._sql = ''
    self._parameters = {}
    self._types = {}
    self._build()

  def _next_param_index(self) -> int:
    return self.param_offset + len(self._parameters)

  def parameters(self) -> Dict[str, Any]:
    return self._parameters

  def sql(self) -> str:
    return self._sql

  def types(self) -> Dict[str, Any]:
    return self._types

  @abc.abstractmethod
  def process_results(self, results: List[Sequence[Any]]) -> ResultType:
    pass

  def _segments(self,
                segment_type: condition.Segment) -> List[condition.Condition]:
    segments = [
        condition for condition in self._conditions
        if condition.segment() == segment_type
    ]
    for segment in segments:
      segment.bind(self._model)
    return segments

  def _build(self) -> None:
    """Builds the Spanner query from the given model and conditions."""
    segment_builders = [
        self._select, self._from, self._where, self._order, self._limit
    ]

    self._sql, self._parameters, self._types = '', {}, {}
    for segment_builder in segment_builders:
      segment_sql, segment_parameters, segment_types = segment_builder()
      self._sql += segment_sql
      self._parameters.update(segment_parameters)
      self._types.update(segment_types)

  @abc.abstractmethod
  def _select(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    """Processes the SELECT segment of the SQL query."""
    pass

  def _from(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    """Processes the FROM segment of the SQL query."""
    froms = self._segments(condition.Segment.FROM)
    index_sql = ''
    if froms:
      if len(froms) != 1:
        raise error.SpannerError('Only one index can be specified')
      force_index = froms[0]
      index_sql = force_index.sql()

    sql = ' FROM {}{}'.format(self._model.table, index_sql)

    return (sql, {}, {})

  def _where(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    """Processes the WHERE segment of the SQL query."""
    sql, sql_parts, parameters, types = '', [], {}, {}
    wheres = self._segments(condition.Segment.WHERE)
    for where in wheres:
      where.suffix = str(self._next_param_index() + len(parameters))
      sql_parts.append(where.sql())
      parameters.update(where.params())
      types.update(where.types())
    if sql_parts:
      sql = ' WHERE {}'.format(' AND '.join(sql_parts))
    return (sql, parameters, types)

  def _order(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    """Processes the ORDER BY segment of the SQL query."""
    sql, parameters, types = '', {}, {}
    orders = self._segments(condition.Segment.ORDER_BY)
    if orders:
      if len(orders) != 1:
        raise error.SpannerError('Only one order condition may be specified')
      order = orders[0]
      order.suffix = str(self._next_param_index())
      sql = ' ' + order.sql()
      parameters = order.params()
      types = order.types()
    return (sql, parameters, types)

  def _limit(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    """Processes the LIMIT segment of the SQL query."""
    sql, parameters, types = '', {}, {}
    limits = self._segments(condition.Segment.LIMIT)
    if limits:
      if len(limits) != 1:
        raise error.SpannerError('Only one limit condition may be specified')
      limit = limits[0]
      limit.suffix = str(self._next_param_index())
      sql = ' ' + limit.sql()
      parameters = limit.params()
      types = limit.types()
    return (sql, parameters, types)


class CountQuery(SpannerQuery[int]):
  """Handles COUNT Spanner queries."""

  def __init__(self, model: Type[Any],
               conditions: Iterable[condition.Condition]):
    super().__init__(model, conditions)
    for c in conditions:
      if c.segment() not in [condition.Segment.WHERE, condition.Segment.FROM]:
        raise error.SpannerError('Only conditions that affect the WHERE or '
                                 'FROM clauses are allowed for count queries')

  def _select(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    return ('SELECT COUNT(*)', {}, {})

  def process_results(self, results: List[Sequence[Any]]) -> int:
    return int(results[0][0])


class SelectQuery(SpannerQuery[List[Type[Any]]]):
  """Handles SELECT Spanner queries."""

  def __init__(self, model: Type[Any],
               conditions: Iterable[condition.Condition]):
    self._model = model
    self._conditions = conditions
    self._joins = self._segments(condition.Segment.JOIN)
    self._subqueries = [
        _SelectSubQuery(join.destination, join.conditions)
        for join in self._joins
        if isinstance(join, condition.IncludesCondition)
    ]
    super().__init__(model, conditions)

  def _select_prefix(self) -> str:
    return 'SELECT'

  def _select(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]:
    parameters, types = {}, {}
    columns = [
        '{alias}.{column}'.format(
            alias=self._model.column_prefix, column=column)
        for column in self._model.columns
    ]
    for subquery in self._subqueries:
      subquery.param_offset = self._next_param_index()
      columns.append('ARRAY({subquery})'.format(subquery=subquery.sql()))
      parameters.update(subquery.parameters())
      types.update(subquery.types())
    return ('{prefix} {columns}'.format(
        prefix=self._select_prefix(),
        columns=', '.join(columns)), parameters, types)

  def process_results(self, results: List[Sequence[Any]]) -> List[Type[Any]]:
    return [self._process_row(result) for result in results]

  def _process_row(self, row: Sequence[Any]) -> Type[Any]:
    """Parses a row of results from a Spanner query based on the conditions."""
    values = dict(zip(self._model.columns, row))
    join_values = row[len(self._model.columns):]
    for join, subquery, join_value in zip(self._joins, self._subqueries,
                                          join_values):
      models = subquery.process_results(join_value)
      if join.single:
        if len(models) > 1:
          raise error.SpannerError(
              'Multiple objects returned for relationship marked as single')
        values[join.relation_name] = models[0] if models else None
      else:
        values[join.relation_name] = models
    return self._model(values, persisted=True)


class _SelectSubQuery(SelectQuery):

  def _select_prefix(self) -> str:
    return 'SELECT AS STRUCT'

Web Proxy Viewer  |  New URL  |  Original Page