| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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 |
| # |
| # http://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. |
| |
| from abc import ABCMeta, abstractproperty |
| from copy import deepcopy |
| from logging import getLogger |
| |
| from tests.comparison.common import Column, TableExpr, TableExprList, ValExpr, ValExprList |
| |
| |
| LOG = getLogger(__name__) |
| |
| |
| class StatementExecutionMode(object): |
| """ |
| Provide a name space for statement execution modes. |
| """ |
| ( |
| # A SELECT statement is executed and results are compared. |
| SELECT_STATEMENT, |
| # If this is chosen, statement execution will run the CTAS statement and then |
| # SELECT * on the table for comparision. The table is torn down after. |
| CREATE_TABLE_AS, |
| # Same as above, except with a few. |
| CREATE_VIEW_AS, |
| # a DML operation that isn't actually a test, but some setup operation that needs |
| # to be run concurrently |
| DML_SETUP, |
| # a DML statement that's actually a test |
| DML_TEST, |
| ) = xrange(5) |
| |
| |
| class AbstractStatement(object): |
| """ |
| Abstract query representation |
| """ |
| |
| __metaclass__ = ABCMeta |
| |
| def __init__(self): |
| # reference to statement's parent. For example the right side of a UNION clause |
| # SELECT will have a parent as the SELECT on the left, which for the query |
| # generator's purpose is the parent |
| self.parent = None |
| # optional WITH clause some statements may have |
| self.with_clause = None |
| self._execution = None |
| |
| @abstractproperty |
| def table_exprs(self): |
| """ |
| Return a list of all table expressions that are declared by this query. This is |
| abstract as the clauses that do this differ across query types. Since all supported |
| queries may have a WITH clause, getting table expressions from the WITH clause is |
| supported here. |
| """ |
| # This is an abstractproperty because it's only a *partial* implementation, however |
| # for any statement or query that has a WITH clause, we can handle that here. |
| table_exprs = TableExprList([]) |
| if self.with_clause: |
| table_exprs.extend(self.with_clause.table_exprs) |
| return table_exprs |
| |
| @abstractproperty |
| def nested_queries(self): |
| """ |
| Returns a list of queries contained within this query. Different queries may have |
| different clauses containing subqueries, so this is an abtract property. |
| """ |
| pass |
| |
| @property |
| def execution(self): |
| """ |
| one of the possible StatementExecutionMode values (see class definition for meaning) |
| """ |
| if self._execution is None: |
| raise Exception('execution is not set on this object') |
| return self._execution |
| |
| @execution.setter |
| def execution(self, val): |
| self._execution = val |
| |
| |
| class Query(AbstractStatement): |
| # TODO: This has to be called Query for as long as we want to unpickle old reports, or |
| # we have to get into the legalese weeds. See: |
| # https://gerrit.cloudera.org/#/c/5162/5/tests/comparison/query.py@61 |
| # https://gerrit.cloudera.org/#/c/5162/1/tests/comparison/leopard/custom_pickle.py@9 |
| # If we decide at some point we don't need to unpickle some of the recent reports, |
| # then this can be renamed to something like SelectStatement. |
| """ |
| A representation of the structure of a SQL SELECT query. Only the select_clause and |
| from_clause are required for a valid query. |
| """ |
| |
| def __init__(self): |
| super(Query, self).__init__() |
| self.select_clause = None |
| self.from_clause = None |
| self.where_clause = None |
| self.group_by_clause = None |
| self.having_clause = None |
| self.union_clause = None |
| self.order_by_clause = None |
| self.limit_clause = None |
| # This is a fine default value, because any well-formed object will be a SELECT |
| # statement. Only the discrepancy searcher makes the decision at run time to change |
| # this. |
| self.execution = StatementExecutionMode.SELECT_STATEMENT |
| |
| def __deepcopy__(self, memo): |
| other = Query() |
| memo[self] = other |
| other.parent = memo[self.parent] if self.parent in memo else None |
| other.with_clause = deepcopy(self.with_clause, memo) |
| other.execution = self.execution |
| other.from_clause = deepcopy(self.from_clause, memo) |
| other.select_clause = deepcopy(self.select_clause, memo) |
| other.where_clause = deepcopy(self.where_clause, memo) |
| other.group_by_clause = deepcopy(self.group_by_clause, memo) |
| other.having_clause = deepcopy(self.having_clause, memo) |
| other.union_clause = deepcopy(self.union_clause, memo) |
| other.order_by_clause = deepcopy(self.order_by_clause, memo) |
| other.limit_clause = deepcopy(self.limit_clause, memo) |
| return other |
| |
| @property |
| def table_exprs(self): |
| '''Provides a list of all table_exprs that are declared by this query. This |
| includes table_exprs in the WITH and FROM sections. |
| ''' |
| table_exprs = super(Query, self).table_exprs # WITH clause |
| table_exprs.extend(self.from_clause.table_exprs) |
| return table_exprs |
| |
| @property |
| def is_unioned_query(self): |
| return self.parent \ |
| and self.parent.union_clause \ |
| and self.parent.union_clause.query is self |
| |
| @property |
| def nested_queries(self): |
| '''Returns a list of queries contained within this query.''' |
| queries = list() |
| if self.with_clause: |
| for inline_view in self.with_clause.with_clause_inline_views: |
| queries.append(inline_view.query) |
| for table_expr in self.table_exprs: |
| if isinstance(table_expr, InlineView): |
| queries.append(table_expr.query) |
| if self.union_clause: |
| queries.append(self.union_clause.query) |
| if self.where_clause: |
| queries.extend( |
| subquery.query for subquery in |
| self.where_clause.boolean_expr.iter_exprs(lambda expr: expr.is_subquery)) |
| for query in list(queries): |
| queries.extend(query.nested_queries) |
| return queries |
| |
| |
| class SelectClause(object): |
| '''This encapsulates the SELECT part of a query. It is convenient to separate |
| non-agg items from agg items so that it is simple to know if the query |
| is an agg query or not. |
| ''' |
| |
| def __init__(self, select_items): |
| self.items = select_items |
| self.distinct = False |
| |
| @property |
| def basic_items(self): |
| '''Returns a list of SelectItems that are also basic items. Deletions from |
| this list will be propagated but additions will not be. |
| ''' |
| return SelectItemSubList(self.items, lambda item: item.is_basic) |
| |
| @property |
| def agg_items(self): |
| '''Returns a list of SelectItems that are also aggregate items. Deletions from |
| this list will be propagated but additions will not be. |
| ''' |
| return SelectItemSubList(self.items, lambda item: item.is_agg) |
| |
| @property |
| def analytic_items(self): |
| '''Returns a list of SelectItems that are also analytic items. Deletions from |
| this list will be propagated but additions will not be. |
| ''' |
| return SelectItemSubList(self.items, lambda item: item.is_analytic) |
| |
| @property |
| def contains_approximate_types(self): |
| '''Returns true if there is a select item that is approximate (such as Float).''' |
| return any(item.type.is_approximate() for item in self.items) |
| |
| def __deepcopy__(self, memo): |
| other = SelectClause([deepcopy(item, memo) for item in self.items]) |
| other.distinct = self.distinct |
| return other |
| |
| |
| # This is used in the query simplifier (not yet checked in) to simplify reduction |
| # of select items. |
| class SelectItemSubList(object): |
| '''A list like object that propagates deletions.''' |
| |
| def __init__(self, select_items, filter): |
| self.select_items = select_items |
| self.filter = filter |
| |
| def __iter__(self): |
| return (item for item in self.select_items if self.filter(item)) |
| |
| def __len__(self): |
| return sum(1 for _ in self) |
| |
| def __nonzero__(self): |
| try: |
| iter(self).next() |
| return True |
| except StopIteration: |
| return False |
| |
| def __getitem__(self, key): |
| if isinstance(key, int): |
| if key < 0: |
| key = len(self) + key |
| if key < 0: |
| raise IndexError() |
| for idx, item in enumerate(self): |
| if idx == key: |
| return item |
| raise IndexError() |
| elif isinstance(key, slice): |
| length = len(self) |
| start, stop, step, reverse = self._get_start_stop_step_reverse(key, length) |
| self_iter = enumerate(self) |
| items = list() |
| while start < stop: |
| try: |
| idx, item = self_iter.next() |
| except StopIteration: |
| break |
| if idx < start: |
| continue |
| elif idx == start: |
| items.append(item) |
| start += step |
| else: |
| break |
| if reverse: |
| items.reverse() |
| return items |
| else: |
| raise TypeError('Index must be a integer or slice, not %s' % key.__class.__name__) |
| |
| def __delitem__(self, key): |
| if isinstance(key, int): |
| if key < 0: |
| key = len(self) + key |
| if key < 0: |
| raise IndexError() |
| for idx, item in enumerate(self.select_items): |
| if not self.filter(item): |
| continue |
| if key == 0: |
| del self.select_items[idx] |
| return |
| key -= 1 |
| raise IndexError() |
| elif isinstance(key, slice): |
| length = len(self) |
| start, stop, step, _ = self._get_start_stop_step_reverse(key, length) |
| self_iter = enumerate(self.select_items) |
| item_idxs = list() |
| filtered_idx = 0 |
| while start < stop: |
| try: |
| idx, item = self_iter.next() |
| except StopIteration: |
| break |
| if not self.filter(item): |
| continue |
| if filtered_idx < start: |
| pass |
| elif filtered_idx == start: |
| item_idxs.append(idx) |
| start += step |
| else: |
| break |
| filtered_idx += 1 |
| item_idxs.reverse() |
| for idx in item_idxs: |
| del self.select_items[idx] |
| else: |
| raise TypeError('Index must be a integer or slice, not %s' % key.__class.__name__) |
| |
| def _get_start_stop_step_reverse(self, slice, length): |
| step = slice.step or 1 |
| if step == 0: |
| raise ValueError('Step cannot be zero') |
| reverse = step < 0 |
| if reverse: |
| step = step * -1 |
| |
| if slice.start is None and slice.stop is None and reverse: |
| return 0, length, step, True |
| |
| if slice.start is None: |
| start = length if reverse else 0 |
| elif slice.start < 0: |
| start = slice.start + length |
| if start < 0: |
| raise IndexError() |
| elif slice.start >= length: |
| start = length - 1 |
| else: |
| start = slice.start |
| |
| if slice.stop is None: |
| stop = 0 if reverse else length |
| elif slice.stop < 0: |
| stop = slice.stop + length |
| if stop < 0: |
| raise IndexError() |
| elif slice.stop > length: |
| stop = length |
| else: |
| stop = slice.stop |
| |
| return start, stop, step, reverse |
| |
| |
| class SelectItem(object): |
| '''A representation of any possible expr than would be valid in |
| |
| SELECT <SelectItem>[, <SelectItem>...] FROM ... |
| |
| Each SelectItem contains a ValExpr which will either be a instance of a |
| DataType (representing a constant), a Column, or a Func. |
| |
| Ex: "SELECT int_col + smallint_col FROM alltypes" would have a val_expr of |
| Plus(Column(<alltypes.int_col>), Column(<alltypes.smallint_col>)). |
| |
| ''' |
| |
| def __init__(self, val_expr, alias=None): |
| self.val_expr = val_expr |
| self.alias = alias |
| |
| @property |
| def name(self): |
| if self.alias: |
| return self.alias |
| if self.val_expr.is_col: |
| return self.val_expr.name |
| raise Exception('Could not determine name') |
| |
| @property |
| def type(self): |
| '''Returns the DataType of this item.''' |
| return self.val_expr.type |
| |
| @property |
| def base_type(self): |
| '''Returns the base DataType of this item.''' |
| return self.val_expr.base_type |
| |
| @property |
| def is_basic(self): |
| '''Evaluates to True if this item is neither an aggregate nor an analytic expression. |
| ''' |
| return not self.is_agg and not self.is_analytic |
| |
| @property |
| def is_agg(self): |
| '''Evaluates to True if this item contains an aggregate expression and does not |
| contain an analytic expression. If an expression contains both an aggregate |
| and an analytic, it is considered an analytic expression. |
| ''' |
| return not self.is_analytic and self.val_expr.contains_agg |
| |
| @property |
| def is_analytic(self): |
| '''Evaluates to True if this item contains an analytic expression.''' |
| return self.val_expr.contains_analytic |
| |
| def __deepcopy__(self, memo): |
| other = SelectItem(deepcopy(self.val_expr, memo)) |
| other.alias = self.alias |
| return other |
| |
| |
| class Subquery(ValExpr): |
| '''Represents both a scalar subquery and a subquery that returns a multi-row/column |
| result set. |
| |
| ''' |
| # TODO: So far it seems fine to use this class for both scalar/non scalar cases but |
| # this could lead to unexpected behavior or be a silent cause of problems... |
| |
| def __init__(self, query): |
| self.query = query |
| |
| @property |
| def type(self): |
| return self.query.select_clause.items[0].type |
| |
| def __deepcopy__(self, memo): |
| return Subquery(deepcopy(self.query, memo)) |
| |
| |
| class FromClause(object): |
| '''A representation of a FROM clause. The member variable join_clauses may optionally |
| contain JoinClause items. |
| ''' |
| |
| def __init__(self, table_expr, join_clauses=None): |
| self.table_expr = table_expr |
| self.join_clauses = join_clauses or list() |
| |
| @property |
| def table_exprs(self): |
| '''Provides a list of all table_exprs that are declared within this FROM |
| block. |
| ''' |
| table_exprs = \ |
| TableExprList(join_clause.table_expr for join_clause in self.join_clauses) |
| table_exprs.append(self.table_expr) |
| return table_exprs |
| |
| def __deepcopy__(self, memo): |
| other = FromClause(deepcopy(self.table_expr, memo)) |
| other.join_clauses = [deepcopy(join_clause, memo) |
| for join_clause in self.join_clauses] |
| return other |
| |
| @property |
| def collections(self): |
| result = self.table_expr.collections |
| for join_clause in self.join_clauses: |
| result.extend(join_clause.table_expr.collections) |
| return result |
| |
| @property |
| def visible_table_exprs(self): |
| '''Provides a list of all table_exprs that are declared within this FROM |
| block and may be referenced in other clauses such as SELECT or WHERE. |
| ''' |
| return TableExprList(table_expr for table_expr in self.table_exprs |
| if table_expr.is_visible) |
| |
| @property |
| def has_non_standard_joins(self): |
| '''Evaluates to True if ANTI or SEMI JOINs are in use.''' |
| if not self.join_clauses: |
| return |
| for join_clause in self.join_clauses: |
| if 'ANTI' in join_clause.join_type or 'SEMI' in join_clause.join_type: |
| return True |
| |
| |
| class InlineView(TableExpr): |
| '''Represents an inline view. |
| |
| Ex: In the query "SELECT * FROM (SELECT * FROM foo) AS bar", |
| "(SELECT * FROM foo) AS bar" would be an inline view. |
| |
| ''' |
| |
| def __init__(self, query): |
| self.query = query |
| self.alias = None |
| self.is_visible = True |
| |
| @property |
| def identifier(self): |
| return self.alias |
| |
| @property |
| def cols(self): |
| return ValExprList(Column(self, item.name, item.type) for item in |
| self.query.select_clause.items) |
| |
| @property |
| def collections(self): |
| return [] |
| |
| def __repr__(self): |
| return '%s<%s>' % (type(self).__name__, ', '.join(repr(col) for col in self.cols)) |
| |
| def __deepcopy__(self, memo): |
| other = InlineView(deepcopy(self.query, memo)) |
| other.alias = self.alias |
| other.is_visible = self.is_visible |
| return other |
| |
| |
| class WithClause(object): |
| '''Represents a WITH clause. |
| |
| Ex: In the query "WITH bar AS (SELECT * FROM foo) SELECT * FROM bar", |
| "WITH bar AS (SELECT * FROM foo)" would be the with clause. |
| |
| ''' |
| |
| def __init__(self, with_clause_inline_views): |
| self.with_clause_inline_views = with_clause_inline_views |
| |
| @property |
| def table_exprs(self): |
| return self.with_clause_inline_views |
| |
| def __deepcopy__(self, memo): |
| return WithClause(deepcopy(self.with_clause_inline_views, memo)) |
| |
| |
| class WithClauseInlineView(InlineView): |
| '''Represents the entries in a WITH clause. These are very similar to InlineViews but |
| may have an additional alias. |
| |
| Ex: WITH bar AS (SELECT * FROM foo) |
| SELECT * |
| FROM bar as r |
| JOIN (SELECT * FROM baz) AS z ON ... |
| |
| The WithClauseInlineView has aliases "bar" and "r" while the InlineView has |
| only the alias "z". |
| |
| ''' |
| |
| def __init__(self, query, with_clause_alias): |
| self.query = query |
| self.with_clause_alias = with_clause_alias |
| self.alias = None |
| |
| @property |
| def identifier(self): |
| return self.alias or self.with_clause_alias |
| |
| def __deepcopy__(self, memo): |
| other = WithClauseInlineView(deepcopy(self.query, memo), self.with_clause_alias) |
| other.alias = self.alias |
| return other |
| |
| |
| class JoinClause(object): |
| '''A representation of a JOIN clause. |
| |
| Ex: SELECT * FROM foo <join_type> JOIN <table_expr> [ON <boolean_expr>] |
| |
| The member variable boolean_expr will be an instance of a boolean func |
| defined below. |
| |
| ''' |
| |
| JOINS_TYPES = [ |
| 'INNER', |
| 'LEFT', |
| 'RIGHT', |
| 'LEFT SEMI', |
| 'LEFT ANTI', |
| 'RIGHT SEMI', |
| 'RIGHT ANTI', |
| 'FULL OUTER', |
| 'CROSS'] |
| |
| def __init__(self, join_type, table_expr, boolean_expr=None): |
| self.join_type = join_type |
| self.table_expr = table_expr |
| self.boolean_expr = boolean_expr |
| # This is used for nested types. It means that we are joining with an earlier aliased |
| # element in the from clause. For example, "From customer t1 INNER JOIN t1.orders t2" |
| # or "FROM customer t1 INNER JOIN t1.orders.lineitems t2 ON t1.comment = t2.comment" |
| # are both lateral joins. However, "FROM customer t1 INNER JOIN customer.orders t2 ON |
| # (t1.comment = t2.comment)" is not a lateral join. |
| # TODO: consider renaming to is_nested_join |
| self.is_lateral_join = False |
| |
| def __deepcopy__(self, memo): |
| other = JoinClause( |
| self.join_type, |
| deepcopy(self.table_expr, memo), |
| deepcopy(self.boolean_expr, memo)) |
| other.is_lateral_join = self.is_lateral_join |
| return other |
| |
| |
| class WhereClause(object): |
| '''The member variable boolean_expr will be an instance of a boolean func |
| defined below. |
| |
| ''' |
| |
| def __init__(self, boolean_expr): |
| self.boolean_expr = boolean_expr |
| |
| def __deepcopy__(self, memo): |
| return WhereClause(deepcopy(self.boolean_expr, memo)) |
| |
| |
| class GroupByClause(object): |
| |
| def __init__(self, group_by_items): |
| self.group_by_items = group_by_items |
| |
| def __deepcopy__(self, memo): |
| return GroupByClause([deepcopy(item, memo) for item in self.group_by_items]) |
| |
| |
| class HavingClause(object): |
| '''The member variable boolean_expr will be an instance of a boolean func |
| defined below. |
| |
| ''' |
| |
| def __init__(self, boolean_expr): |
| self.boolean_expr = boolean_expr |
| |
| def __deepcopy__(self, memo): |
| return HavingClause(deepcopy(self.boolean_expr, memo)) |
| |
| |
| class UnionClause(object): |
| '''A representation of a UNION clause. |
| |
| If the member variable "all" is True, the instance represents a "UNION ALL". |
| |
| ''' |
| |
| def __init__(self, query): |
| self.query = query |
| self.all = False |
| |
| @property |
| def queries(self): |
| queries = list() |
| query = self.query |
| while True: |
| queries.append(query) |
| if not query.union_clause: |
| break |
| query = query.union_clause.query |
| return queries |
| |
| def __deepcopy__(self, memo): |
| other = UnionClause(deepcopy(self.query, memo)) |
| other.all = self.all |
| return other |
| |
| |
| class OrderByClause(object): |
| |
| def __init__(self, val_exprs): |
| '''val_exprs must be a list containing either ValExprs or a tuple of (ValExpr, |
| String). If plain ValExprs are used, the order will be ASC. If tuples are used, |
| the string must be either ASC or DESC. |
| ''' |
| self.exprs_to_order = list() |
| for item in val_exprs: |
| try: |
| order = val_exprs[item] |
| except TypeError: # not a dict |
| order = 'ASC' |
| self.exprs_to_order.append((item, order)) |
| |
| def __deepcopy__(self, memo): |
| other = OrderByClause(val_exprs=list()) |
| for (item, order) in self.exprs_to_order: |
| other.exprs_to_order.append((deepcopy(item, memo), order)) |
| return other |
| |
| |
| class LimitClause(object): |
| |
| def __init__(self, limit): |
| self.limit = limit |
| |
| def __deepcopy__(self, memo): |
| return LimitClause(deepcopy(self.limit, memo)) |
| |
| |
| class InsertClause(object): |
| |
| # This enum represents possibilities for different types of INSERTs. A user of this |
| # object, like StatementGenerator, is responsible for setting the conflict_action |
| # value appropriately. These values are valid for the conflict_action parameter. |
| # Because an InsertStatement is a single piece of data shared across multiple SQL |
| # dialects, this setting can alter the written SQL in multiple dialects. |
| # |
| # CONLICT_ACTION_DEFAULT |
| # |
| # For Impala, this is a statement like INSERT INTO hdfs_table SELECT * FROM foo |
| # For PostgreSQL, this is a statement like INSERT INTO hdfs_table SELECT * FROM foo |
| # |
| # Example uses cases: inserting into tables that do not have primary keys, or |
| # inserting into PostgreSQL tables where you want to error if there are attempts to |
| # insert duplicate primary keys |
| # |
| # CONFLICT_ACTION_IGNORE |
| # |
| # For Impala, this is a statement like INSERT INTO kudu_table SELECT * FROM foo |
| # For PostgreSQL, this is a statement like INSERT INTO kudu_table SELECT * FROM foo |
| # ON CONFLICT DO NOTHING |
| # |
| # Example use case: inserting into Kudu tables, where attempts to insert duplicate |
| # primary key rows are ignored by Impala, so they must also be ignored by PostgreSQL. |
| # Note that the *syntax* for INSERT doesn't change with Impala, but because it's a |
| # Kudu table, the behavior differs. |
| # |
| # CONFLICT_ACTION_UPDATE |
| # |
| # For Impala, this is a statement like UPSERT INTO kudu_table SELECT * FROM foo |
| # For PostgreSQL, this is a statement like INSERT INTO kudu_table SELECT * FROM foo |
| # ON CONFLICT DO UPDATE SET |
| # (col1 = EXCLUDED.col1, ...) |
| # |
| # Example use case: upserting into Kudu tables, where attempts to insert duplicate |
| # primary key rows will either insert a single row, or update a single row already |
| # there, without error. In PostgreSQL, UPSERT is written via this "ON CONFLICT DO |
| # UPDATE" clause. |
| # |
| # More on PostgreSQL INSERT/UPSERT syntax here: |
| # https://www.postgresql.org/docs/9.5/static/sql-insert.html |
| |
| (CONFLICT_ACTION_DEFAULT, |
| CONFLICT_ACTION_IGNORE, |
| CONFLICT_ACTION_UPDATE) = range(3) |
| |
| def __init__(self, table, column_list=None, conflict_action=CONFLICT_ACTION_DEFAULT): |
| """ |
| Represent an INSERT/UPSERT clause, which is the first half of an INSERT/UPSERT |
| statement. Note that UPSERTs are very similar to INSERTs, so this data structure can |
| easily deal with both. |
| |
| The table is a Table object. |
| |
| column_list is an optional list, tuple, or other sequence of |
| tests.comparison.common.Column objects. In an Impala INSERT/UPSERT SQL statement, |
| it's a sequence of column names. See |
| http://www.cloudera.com/documentation/enterprise/latest/topics/impala_insert.html |
| |
| conflict_action takes in one of the CONFLICT_ACTION_* class attributes. See above. |
| """ |
| self.table = table |
| self.column_list = column_list |
| self.conflict_action = conflict_action |
| |
| |
| class ValuesRow(object): |
| def __init__(self, items): |
| """ |
| Represent a single row in a VALUES clause. The items are literals or expressions. |
| """ |
| self.items = items |
| |
| |
| class ValuesClause(object): |
| def __init__(self, values_rows): |
| """ |
| Represent the VALUES clause of an INSERT/UPSERT statement. The values_rows is a |
| sequence of ValuesRow objects. |
| """ |
| self.values_rows = values_rows |
| |
| |
| class InsertStatement(AbstractStatement): |
| |
| def __init__(self, with_clause=None, insert_clause=None, select_query=None, |
| values_clause=None, execution=None): |
| """ |
| Represent an INSERT/UPSERT statement. Note that UPSERTs are very similar to INSERTs, |
| so this data structure can easily deal with both. |
| |
| The INSERT/UPSERT may have an optional WithClause, and then either a SELECT query |
| (Query) object from whose rows we INSERT, or a VALUES clause, but not both. |
| |
| The execution attribute is used by the discrepancy_searcher to track whether this |
| InsertStatement is some sort of setup operation or a true random statement test. |
| """ |
| super(InsertStatement, self).__init__() |
| self._select_query = None |
| self._values_clause = None |
| self.execution = execution |
| self.select_query = select_query |
| self.values_clause = values_clause |
| self.with_clause = with_clause |
| self.insert_clause = insert_clause |
| |
| @property |
| def select_query(self): |
| return self._select_query |
| |
| @select_query.setter |
| def select_query(self, select_query): |
| if self.values_clause is None or select_query is None: |
| self._select_query = select_query |
| else: |
| raise Exception('An INSERT/UPSERT statement may not have both the select_query and ' |
| 'values_clause set: {select}; {values}'.format( |
| select=select_query, values=self.values_clause)) |
| |
| @property |
| def values_clause(self): |
| return self._values_clause |
| |
| @values_clause.setter |
| def values_clause(self, values_clause): |
| if self.select_query is None or values_clause is None: |
| self._values_clause = values_clause |
| else: |
| raise Exception('An INSERT/UPSERT statement may not have both the select_query and ' |
| 'values_clause set: {select}; {values}'.format( |
| select=self.select_query, values=values_clause)) |
| |
| @property |
| def table_exprs(self): |
| table_exprs = super(InsertStatement, self).table_exprs # WITH clause |
| if self.select_query is not None: |
| table_exprs.extend(self.select_query.table_exprs) |
| return table_exprs |
| |
| @property |
| def nested_queries(self): |
| queries = list() |
| if self.with_clause is not None: |
| for inline_view in self.with_clause.with_clause_inline_views: |
| queries.append(inline_view.query) |
| if self.select_query is not None: |
| queries.append(self.select_query) |
| queries.extend(self.select_query.nested_queries) |
| return queries |
| |
| @property |
| def dml_table(self): |
| return self.insert_clause.table |
| |
| @property |
| def conflict_action(self): |
| return self.insert_clause.conflict_action |
| |
| @property |
| def primary_key_string(self): |
| return '({primary_key_list})'.format( |
| primary_key_list=', '.join(self.insert_clause.table.primary_key_names)) |
| |
| @property |
| def updatable_column_names(self): |
| return self.insert_clause.table.updatable_column_names |