Skip to content

Commit

Permalink
Allow indexing named tuples by position; fix named tuples serialization
Browse files Browse the repository at this point in the history
Named tuples can now be indexed by position.  The below two queries are
equivalent:

    db> SELECT (a:=1).a;
    db> SELECT (a:=1).0;

Named tuples will now be correctly serialized to JSON in more cases,
for example:

    db> SELECT [(a:=1)][0];
    {"a": 1}

Serialization of nested named tuples is still broken in some cases
though:

    db> SELECT [(a:=(x:=1))][0];
    {"a": {"f1": 1}}
  • Loading branch information
1st1 committed Oct 31, 2018
1 parent 580bd46 commit 3e0832c
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 88 deletions.
19 changes: 8 additions & 11 deletions edb/lang/edgeql/compiler/setgen.py
Expand Up @@ -461,17 +461,14 @@ def tuple_indirection_set(
else:
el_name = ptr_name[1]

if el_name in source.element_types:
path_id = irutils.tuple_indirection_path_id(
path_tip.path_id, el_name,
source.element_types[el_name])
expr = irast.TupleIndirection(
expr=path_tip, name=el_name, path_id=path_id,
context=source_context)
else:
raise errors.EdgeQLReferenceError(
f'{el_name} is not a member of a tuple',
context=source_context)
el_norm_name = source.normalize_index(el_name)
el_type = source.get_subtype(el_name)

path_id = irutils.tuple_indirection_path_id(
path_tip.path_id, el_norm_name, el_type)
expr = irast.TupleIndirection(
expr=path_tip, name=el_norm_name, path_id=path_id,
context=source_context)

return generated_set(expr, ctx=ctx)

Expand Down
5 changes: 2 additions & 3 deletions edb/lang/ir/inference/types.py
Expand Up @@ -39,8 +39,7 @@
def amend_empty_set_type(es: irast.EmptySet, t: s_obj.Object, schema) -> None:
alias = es.path_id.target.name.name
scls_name = s_name.Name(module='__expr__', name=alias)
scls = t.__class__(name=scls_name, bases=[t])
scls.acquire_ancestor_inheritance(schema)
scls = t.derive_subtype(schema, name=scls_name)
es.path_id = irast.PathId(scls)
es.scls = t

Expand Down Expand Up @@ -441,7 +440,7 @@ def __infer_struct(ir, schema):
@_infer_type.register(irast.TupleIndirection)
def __infer_struct_indirection(ir, schema):
struct_type = infer_type(ir.expr, schema)
result = struct_type.element_types.get(ir.name)
result = struct_type.get_subtype(ir.name)
if result is None:
raise ql_errors.EdgeQLError('could not determine struct element type',
context=ir.context)
Expand Down
2 changes: 1 addition & 1 deletion edb/lang/schema/_std.eql
Expand Up @@ -115,7 +115,7 @@ CREATE FUNCTION std::array_contains(array: array<any>,
$$;

CREATE FUNCTION std::array_enumerate(array: array<any>) ->
SET OF tuple<any, std::int64>
SET OF tuple<element:any, index:std::int64>
FROM SQL FUNCTION '--system--';

CREATE FUNCTION std::array_get(array: array<any>,
Expand Down
5 changes: 5 additions & 0 deletions edb/lang/schema/nodes.py
Expand Up @@ -35,6 +35,11 @@ def material_type(self):
t = t.bases[0]
return t

def derive_subtype(self, schema, *, name: str) -> s_types.Type:
st = type(self)(name=name, bases=[self])
st.acquire_ancestor_inheritance(schema)
return st

def peel_view(self):
if self.is_view():
return self.bases[0]
Expand Down
3 changes: 3 additions & 0 deletions edb/lang/schema/objtypes.py
Expand Up @@ -39,6 +39,9 @@ class SourceNode(sources.Source, nodes.Node):
class ObjectType(SourceNode, constraints.ConsistencySubject):
_type = 'ObjectType'

def is_object_type(self):
return True

def materialize_policies(self, schema):
bases = self.bases

Expand Down
67 changes: 63 additions & 4 deletions edb/lang/schema/types.py
Expand Up @@ -53,9 +53,15 @@ class Type(so.NamedObject, derivable.DerivableObjectBase):
# rptr will contain the inbound pointer class.
rptr = so.Field(so.Object, default=None, compcoef=0.909)

def derive_subtype(self, schema, *, name: str) -> 'Type':
raise NotImplementedError

def is_type(self):
return True

def is_object_type(self):
return False

def is_polymorphic(self):
return False

Expand Down Expand Up @@ -298,6 +304,12 @@ def is_array(self):
def get_container(self):
return tuple

def derive_subtype(self, schema, *, name: str) -> Type:
return Array.from_subtypes(
self.element_type,
self.get_typemods(),
name=name)

def get_subtypes(self):
return (self.element_type,)

Expand Down Expand Up @@ -365,7 +377,7 @@ def _test_polymorphic(self, other: 'Type'):
return self.element_type.test_polymorphic(other.element_type)

@classmethod
def from_subtypes(cls, subtypes, typemods=None):
def from_subtypes(cls, subtypes, typemods=None, *, name=None):
if len(subtypes) != 1:
raise s_err.SchemaError(
f'unexpected number of subtypes, expecting 1: {subtypes!r}')
Expand All @@ -389,7 +401,7 @@ def from_subtypes(cls, subtypes, typemods=None):
element_type = stype
dimensions = []

return cls(element_type=element_type, dimensions=dimensions)
return cls(element_type=element_type, dimensions=dimensions, name=name)

def __hash__(self):
return hash((
Expand Down Expand Up @@ -422,14 +434,61 @@ def is_tuple(self):
def get_container(self):
return dict

def iter_subtypes(self):
yield from self.element_types.items()

def normalize_index(self, field: str) -> str:
if self.named and field.isdecimal():
idx = int(field)
if idx >= 0 and idx < len(self.element_types):
return list(self.element_types.keys())[idx]
else:
raise s_err.ItemNotFoundError(
f'{field} is not a member of {self.displayname}')

return field

def index_of(self, field: str) -> int:
if field.isdecimal():
idx = int(field)
if idx >= 0 and idx < len(self.element_types):
if self.named:
return list(self.element_types.keys()).index(field)
else:
return idx
elif self.named and field in self.element_types:
return list(self.element_types.keys()).index(field)

raise s_err.ItemNotFoundError(
f'{field} is not a member of {self.displayname}')

def get_subtype(self, field: str) -> Type:
# index can be a name or a position
if field.isdecimal():
idx = int(field)
if idx >= 0 and idx < len(self.element_types):
return list(self.element_types.values())[idx]

elif self.named and field in self.element_types:
return self.element_types[field]

raise s_err.ItemNotFoundError(
f'{field} is not a member of {self.displayname}')

def get_subtypes(self):
if self.element_types:
return list(self.element_types.values())
else:
return []

def derive_subtype(self, schema, *, name: str) -> Type:
return Tuple.from_subtypes(
self.element_types,
self.get_typemods(),
name=name)

@classmethod
def from_subtypes(cls, subtypes, typemods=None):
def from_subtypes(cls, subtypes, typemods=None, *, name: str=None):
named = False
if typemods is not None:
named = typemods.get('named', False)
Expand All @@ -441,7 +500,7 @@ def from_subtypes(cls, subtypes, typemods=None):
else:
types = subtypes

return cls(element_types=types, named=named)
return cls(element_types=types, named=named, name=name)

def implicitly_castable_to(self, other: Type, schema) -> bool:
if not other.is_tuple():
Expand Down
41 changes: 37 additions & 4 deletions edb/server/pgsql/compiler/output.py
Expand Up @@ -22,13 +22,42 @@
import typing

from edb.lang.ir import ast as irast

from edb.lang.schema import objtypes as s_objtypes
from edb.lang.schema import types as s_types

from edb.server.pgsql import ast as pgast
from edb.server.pgsql import types as pgtypes

from . import context
from . import typecomp


def named_tuple_as_json_object(expr, *, stype, env):
assert stype.is_tuple() and stype.named

keyvals = []
for el_idx, (el_name, el_type) in enumerate(stype.iter_subtypes()):
keyvals.append(pgast.StringConstant(val=el_name))

type_sentinel = typecomp.cast(
pgast.NullConstant(),
source_type=el_type, target_type=el_type, force=True,
env=env)

val = pgast.FuncCall(
name=('edgedb', 'row_getattr_by_num'),
args=[
expr,
pgast.NumericConstant(val=str(el_idx + 1)),
type_sentinel
])

keyvals.append(val)

return pgast.FuncCall(
name=('jsonb_build_object',),
args=keyvals, null_safe=True, nullable=expr.nullable)


def tuple_var_as_json_object(tvar, *, path_id, env):
Expand Down Expand Up @@ -113,10 +142,14 @@ def serialize_expr_to_json(
name=('jsonb_build_array',), args=expr.args,
null_safe=True)

elif isinstance(path_id.target, s_types.Tuple):
val = pgast.FuncCall(
name=('edgedb', 'row_to_jsonb_array',), args=[expr],
null_safe=True)
elif path_id.target.is_tuple():
if path_id.target.named:
val = named_tuple_as_json_object(
expr, stype=path_id.target, env=env)
else:
val = pgast.FuncCall(
name=('edgedb', 'row_to_jsonb_array',), args=[expr],
null_safe=True)

elif not nested:
val = pgast.FuncCall(
Expand Down
10 changes: 4 additions & 6 deletions edb/server/pgsql/compiler/relgen.py
Expand Up @@ -1163,7 +1163,7 @@ def process_set_as_tuple(
for element in expr.elements:
path_id = irutils.tuple_indirection_path_id(
ir_set.path_id, element.name,
ir_set.scls.element_types[element.name]
ir_set.scls.get_subtype(element.name)
)
stmt.view_path_id_map[path_id] = element.val.path_id

Expand Down Expand Up @@ -1218,10 +1218,8 @@ def process_set_as_tuple_indirection(
source_type=ir_set.scls, target_type=ir_set.scls, force=True,
env=subctx.env)

tuple_atts = list(tuple_set.scls.element_types.keys())
att_idx = pgast.NumericConstant(
val=str(tuple_atts.index(ir_set.expr.name) + 1)
)
index = tuple_set.scls.index_of(ir_set.expr.name)
att_idx = pgast.NumericConstant(val=str(index + 1))

set_expr = pgast.FuncCall(
name=('edgedb', 'row_getattr_by_num'),
Expand Down Expand Up @@ -1373,7 +1371,7 @@ def process_set_as_func_expr(
elements=[
pgast.TupleElement(
path_id=irutils.tuple_indirection_path_id(
ir_set.path_id, n, rtype.element_types[n],
ir_set.path_id, n, rtype.get_subtype(n),
),
name=n,
val=dbobj.get_column(
Expand Down

0 comments on commit 3e0832c

Please sign in to comment.