Skip to content

Commit

Permalink
Expose EdgeQL computed pointers over SQL adapter (#7443)
Browse files Browse the repository at this point in the history
  • Loading branch information
aljazerzen authored Jun 11, 2024
1 parent 720e95a commit 6070769
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 68 deletions.
188 changes: 148 additions & 40 deletions edb/pgsql/metaschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5887,24 +5887,76 @@ def _generate_sql_information_schema() -> List[dbops.Command]:
-- fallback to pointer name, with suffix '_id' for links
sp.name || case when sl.id is not null then '_id' else '' end
) AS v_column_name,
COALESCE(spec.position, 2) as position,
isc.*
FROM information_schema.columns isc
JOIN edgedbsql_VER.virtual_tables vt ON vt.id::text = isc.table_name
COALESCE(spec.position, 2) AS position,
(sp.expr IS NOT NULL) AS is_computed,
isc.column_default,
CASE WHEN sp.required OR spec.k IS NOT NULL
THEN 'NO' ELSE 'YES' END AS is_nullable,
-- HACK: computeds don't have backing rows in isc,
-- so we just default to 'text'. This is wrong.
COALESCE(isc.data_type, 'text') AS data_type
FROM edgedb_VER."_SchemaPointer" sp
LEFT JOIN information_schema.columns isc ON (
isc.table_name = sp.source::TEXT AND CASE
WHEN length(isc.column_name) = 36 -- if column name is uuid
THEN isc.column_name = sp.id::text -- compare uuids
ELSE isc.column_name = sp.name -- for id, source, target
END
)
-- needed for attaching `_id`
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id = sp.id
-- needed for determining table name
JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.source
-- id is duplicated to get id and __type__ columns out of it
-- positions for special pointers
-- duplicate id get both id and __type__ columns out of it
LEFT JOIN (
VALUES ('id', 'id', 0),
('id', '__type__', 1),
('source', 'source', 0),
('target', 'target', 1)
) spec(k, name, position) ON (spec.k = isc.column_name)
LEFT JOIN edgedb_VER."_SchemaPointer" sp
ON sp.id::text = isc.column_name
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id::text = isc.column_name
WHERE isc.column_name IS NOT NULL -- normal pointers
OR sp.expr IS NOT NULL AND sp.cardinality <> 'Many' -- computeds
UNION ALL
-- special case: multi properties source and target
-- (this is needed, because schema does not create pointers for
-- these two columns)
SELECT
vt.schema_name AS vt_table_schema,
vt.table_name AS vt_table_name,
isc.column_name AS v_column_name,
spec.position as position,
FALSE as is_computed,
isc.column_default,
'NO' as is_nullable,
isc.data_type as data_type
FROM edgedb_VER."_SchemaPointer" sp
JOIN information_schema.columns isc ON isc.table_name = sp.id::TEXT
-- needed for filtering out links
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id = sp.id
-- needed for determining table name
JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.id
-- positions for special pointers
JOIN (
VALUES ('source', 'source', 0),
('target', 'target', 1)
) spec(k, name, position) ON (spec.k = isc.column_name)
WHERE
sl.id IS NULL -- property (non-link)
AND sp.cardinality = 'Many' -- multi
AND sp.expr IS NULL -- non-computed
) t
WHERE v_column_name IS NOT NULL
'''
),
),
Expand Down Expand Up @@ -6182,32 +6234,32 @@ def _generate_sql_information_schema() -> List[dbops.Command]:
UNION ALL
SELECT attrelid,
SELECT pc_oid as attrelid,
col_name as attname,
atttypid,
attstattarget,
attlen,
COALESCE(atttypid, 25) as atttypid, -- defaults to TEXT
COALESCE(attstattarget, -1) as attstattarget,
COALESCE(attlen, -1) as attlen,
(ROW_NUMBER() OVER (
PARTITION BY attrelid
PARTITION BY pc_oid
ORDER BY col_position, col_name
) - 6)::smallint AS attnum,
t.attnum as attnum_internal,
attndims,
attcacheoff,
atttypmod,
attbyval,
attstorage,
attalign,
attnotnull,
COALESCE(attndims, 0) as attndims,
COALESCE(attcacheoff, -1) as attcacheoff,
COALESCE(atttypmod, -1) as atttypmod,
COALESCE(attbyval, FALSE) as attbyval,
COALESCE(attstorage, 'x') as attstorage,
COALESCE(attalign, 'i') as attalign,
required as attnotnull,
-- Always report no default, to avoid expr trouble
false as atthasdef,
atthasmissing,
attidentity,
attgenerated,
attisdropped,
attislocal,
attinhcount,
attcollation,
COALESCE(atthasmissing, FALSE) as atthasmissing,
COALESCE(attidentity, '') as attidentity,
COALESCE(attgenerated, '') as attgenerated,
COALESCE(attisdropped, FALSE) as attisdropped,
COALESCE(attislocal, TRUE) as attislocal,
COALESCE(attinhcount, 0) as attinhcount,
COALESCE(attcollation, 0) as attcollation,
attacl,
attoptions,
attfdwoptions,
Expand All @@ -6220,28 +6272,84 @@ def _generate_sql_information_schema() -> List[dbops.Command]:
sp.name || case when sl.id is not null then '_id' else '' end,
pa.attname -- for system columns
) as col_name,
CASE
WHEN pa.attnum <= 0 THEN pa.attnum
ELSE COALESCE(spec.position, 2)
END as col_position,
COALESCE(spec.position, 2) AS col_position,
(sp.required IS TRUE OR spec.k IS NOT NULL) as required,
pc.oid AS pc_oid,
pa.*,
pa.tableoid, pa.xmin, pa.cmin, pa.xmax, pa.cmax, pa.ctid
FROM pg_attribute pa
JOIN pg_class pc ON pc.oid = pa.attrelid
JOIN edgedbsql_VER.virtual_tables vt ON vt.backend_id = pc.reltype
-- id is duplicated to get id and __type__ columns out of it
FROM edgedb_VER."_SchemaPointer" sp
JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.source
JOIN pg_class pc ON pc.reltype = vt.backend_id
-- try to find existing pg_attribute (it will not exist for computeds)
LEFT JOIN pg_attribute pa ON (
pa.attrelid = pc.oid AND CASE
WHEN length(pa.attname) = 36 -- if column name is uuid
THEN pa.attname = sp.id::text -- compare uuids
ELSE pa.attname = sp.name -- for id, source, target
END
)
-- positions for special pointers
-- duplicate id get both id and __type__ columns out of it
LEFT JOIN (
VALUES ('id', 'id', 0),
('id', '__type__', 1),
('source', 'source', 0),
('target', 'target', 1)
) spec(k, name, position) ON (spec.k = pa.attname)
LEFT JOIN edgedb_VER."_SchemaPointer" sp ON sp.id::text = pa.attname
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id::text = pa.attname
-- Filter out internal columns
WHERE pa.attname NOT LIKE '\_\_%\_\_' OR pa.attname = '__type__'
-- needed for attaching `_id`
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id = sp.id
WHERE pa.attname IS NOT NULL -- non-computed pointers
OR sp.expr IS NOT NULL AND sp.cardinality <> 'Many' -- computeds
UNION ALL
-- special case: multi properties source and target
-- (this is needed, because schema does not create pointers for
-- these two columns)
SELECT
pa.attname AS col_name,
spec.position as position,
TRUE as required,
pa.attrelid as pc_oid,
pa.*,
pa.tableoid, pa.xmin, pa.cmin, pa.xmax, pa.cmax, pa.ctid
FROM edgedb_VER."_SchemaPointer" sp
JOIN pg_class pc ON pc.relname = sp.id::TEXT
JOIN pg_attribute pa ON pa.attrelid = pc.oid
-- needed for filtering out links
LEFT JOIN edgedb_VER."_SchemaLink" sl ON sl.id = sp.id
-- positions for special pointers
JOIN (
VALUES ('source', 0),
('target', 1)
) spec(k, position) ON (spec.k = pa.attname)
WHERE
sl.id IS NULL -- property (non-link)
AND sp.cardinality = 'Many' -- multi
AND sp.expr IS NULL -- non-computed
UNION ALL
-- special case: system columns
SELECT
pa.attname AS col_name,
pa.attnum as position,
TRUE as required,
pa.attrelid as pc_oid,
pa.*,
pa.tableoid, pa.xmin, pa.cmin, pa.xmax, pa.cmax, pa.ctid
FROM pg_attribute pa
JOIN pg_class pc ON pc.oid = pa.attrelid
JOIN edgedbsql_VER.virtual_tables vt ON vt.backend_id = pc.reltype
WHERE pa.attnum < 0
) t
""",
),
Expand Down
9 changes: 8 additions & 1 deletion edb/pgsql/resolver/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def resolve_CopyStmt(stmt: pgast.CopyStmt, *, ctx: Context) -> pgast.CopyStmt:

elif stmt.relation:
relation, table = dispatch.resolve_relation(stmt.relation, ctx=ctx)
table.reference_as = ctx.names.get('rel')

if stmt.colnames:
col_map: Dict[str, context.Column] = {
col.name: col for col in table.columns
Expand All @@ -54,7 +56,12 @@ def resolve_CopyStmt(stmt: pgast.CopyStmt, *, ctx: Context) -> pgast.CopyStmt:
# This is probably a view based on edgedb schema, so wrap it into
# a select query.
query = pgast.SelectStmt(
from_clause=[pgast.RelRangeVar(relation=relation)],
from_clause=[
pgast.RelRangeVar(
alias=pgast.Alias(aliasname=table.reference_as),
relation=relation,
)
],
target_list=[
pgast.ResTarget(
val=pg_res_expr.resolve_column_kind(table, c.kind, ctx=ctx)
Expand Down
13 changes: 13 additions & 0 deletions edb/pgsql/resolver/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
import enum
import uuid

from edb.pgsql import ast as pgast

from edb.common import compiler
from edb.schema import schema as s_schema
from edb.schema import pointers as s_pointers


@dataclass(frozen=True)
Expand Down Expand Up @@ -129,6 +132,13 @@ class ColumnStaticVal(ColumnKind):
val: uuid.UUID


@dataclass(kw_only=True)
class ColumnComputable(ColumnKind):
# An EdgeQL computable property. To get the AST for this column, EdgeQL
# compiler needs to be invoked.
pointer: s_pointers.Pointer


class ContextSwitchMode(enum.Enum):
EMPTY = enum.auto()
CHILD = enum.auto()
Expand All @@ -146,6 +156,9 @@ class ResolverContextLevel(compiler.ContextLevel):
# child objects.
include_inherited: bool

# List of CTEs to append to the current SELECT statement
cte_to_append: List[pgast.CommonTableExpr]

options: Options

def __init__(
Expand Down
52 changes: 49 additions & 3 deletions edb/pgsql/resolver/expr.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.
Expand Down Expand Up @@ -26,6 +27,14 @@

from edb.pgsql import ast as pgast
from edb.pgsql import trampoline
from edb.pgsql import compiler as pgcompiler

from edb.schema import types as s_types

from edb.ir import ast as irast


from edb.edgeql import compiler as qlcompiler

from . import dispatch
from . import context
Expand Down Expand Up @@ -117,6 +126,39 @@ def resolve_column_kind(
case context.ColumnStaticVal(val=val):
# special case: __type__ static value
return _uuid_const(val)
case context.ColumnComputable(pointer=pointer):

expr = pointer.get_expr(ctx.schema)
assert expr

source = pointer.get_source(ctx.schema)
assert isinstance(source, s_types.Type)
source_id = irast.PathId.from_type(ctx.schema, source, env=None)

singletons = [source]
options = qlcompiler.CompilerOptions(
modaliases={None: 'default'},
anchors={'__source__': source},
path_prefix_anchor='__source__',
singletons=singletons,
make_globals_empty=True, # TODO: globals in SQL
)
compiled = expr.compiled(ctx.schema, options=options)

sql_tree = pgcompiler.compile_ir_to_sql_tree(
compiled.irast,
external_rvars={
(source_id, 'source'): pgast.RelRangeVar(
alias=pgast.Alias(
aliasname=table.reference_as,
),
relation=pgast.Relation(name=table.reference_as),
),
},
output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,
)
assert isinstance(sql_tree.ast, pgast.BaseExpr)
return sql_tree.ast
case _:
raise NotImplementedError(column)

Expand Down Expand Up @@ -174,7 +216,7 @@ def _lookup_column(
assert tab.reference_as
col = context.Column(
name=tab.reference_as,
kind=context.ColumnByName(reference_as=tab.reference_as)
kind=context.ColumnByName(reference_as=tab.reference_as),
)
return [(context.Table(), col)]
except errors.QueryError:
Expand Down Expand Up @@ -338,9 +380,13 @@ def resolve_SortBy(

func_calls_remapping: Dict[Tuple[str, ...], Tuple[str, ...]] = {
('information_schema', '_pg_truetypid'): (
trampoline.versioned_schema('edgedbsql'), '_pg_truetypid'),
trampoline.versioned_schema('edgedbsql'),
'_pg_truetypid',
),
('information_schema', '_pg_truetypmod'): (
trampoline.versioned_schema('edgedbsql'), '_pg_truetypmod'),
trampoline.versioned_schema('edgedbsql'),
'_pg_truetypmod',
),
('pg_catalog', 'format_type'): ('edgedb', '_format_type'),
}

Expand Down
Loading

0 comments on commit 6070769

Please sign in to comment.