Skip to content

Commit

Permalink
Emulate foreign keys for multi tables over SQL adapter (#7994)
Browse files Browse the repository at this point in the history
  • Loading branch information
aljazerzen authored Nov 18, 2024
1 parent bd2cb47 commit 4b339e5
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 24 deletions.
120 changes: 96 additions & 24 deletions edb/pgsql/metaschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6682,22 +6682,18 @@ def _generate_sql_information_schema(
pa.attrelid as pc_oid,
pa.*,
pa.tableoid, pa.xmin, pa.cmin, pa.xmax, pa.cmax, pa.ctid
FROM edgedb_VER."_SchemaPointer" sp
FROM edgedb_VER."_SchemaProperty" 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
sp.cardinality = 'Many' -- multi
AND sp.expr IS NULL -- non-computed
UNION ALL
Expand Down Expand Up @@ -6812,7 +6808,10 @@ def _generate_sql_information_schema(
trampoline.VersionedView(
name=("edgedbsql", "pg_constraint"),
query=r"""
-- primary keys
-- primary keys for:
-- - objects tables (that contains id)
-- - link tables (that contains source and target)
-- there exists a unique constraint for each of these
SELECT
pc.oid,
vt.table_name || '_pk' AS conname,
Expand All @@ -6825,32 +6824,38 @@ def _generate_sql_information_schema(
pc.contypid,
pc.conindid,
pc.conparentid,
pc.confrelid,
pc.confupdtype,
pc.confdeltype,
pc.confmatchtype,
NULL::oid AS confrelid,
NULL::"char" AS confupdtype,
NULL::"char" AS confdeltype,
NULL::"char" AS confmatchtype,
pc.conislocal,
pc.coninhcount,
pc.connoinherit,
pc.conkey,
pc.confkey,
pc.conpfeqop,
pc.conppeqop,
pc.conffeqop,
pc.confdelsetcols,
pc.conexclop,
CASE WHEN pa.attname = 'id'
THEN ARRAY[1]::int2[] -- id will always have attnum 1
ELSE ARRAY[1, 2]::int2[] -- source and target
END AS conkey,
NULL::int2[] AS confkey,
NULL::oid[] AS conpfeqop,
NULL::oid[] AS conppeqop,
NULL::oid[] AS conffeqop,
NULL::int2[] AS confdelsetcols,
NULL::oid[] AS conexclop,
pc.conbin,
pc.tableoid, pc.xmin, pc.cmin, pc.xmax, pc.cmax, pc.ctid
FROM pg_constraint pc
JOIN edgedbsql_VER.pg_class_tables pct ON pct.oid = pc.conrelid
JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pct.reltype
JOIN pg_attribute pa ON (pa.attname = 'id' AND pa.attrelid = pct.oid)
JOIN pg_attribute pa
ON (pa.attrelid = pct.oid
AND pa.attnum = ANY(conkey)
AND pa.attname IN ('id', 'source')
)
WHERE contype = 'u' -- our ids and all links will have unique constraint
AND attnum = ANY(conkey)
UNION ALL
-- foreign keys
-- foreign keys for object tables
SELECT
edgedbsql_VER.uuid_to_oid(sl.id) as oid,
vt.table_name || '_fk_' || sl.name AS conname,
Expand All @@ -6872,9 +6877,9 @@ def _generate_sql_information_schema(
TRUE AS connoinherit,
ARRAY[pa.attnum]::int2[] AS conkey,
ARRAY[1]::int2[] AS confkey, -- id will always have attnum 1
ARRAY[2972]::oid[] AS conpfeqop, -- 2972 is eq comparison for uuids
ARRAY[2972]::oid[] AS conppeqop, -- 2972 is eq comparison for uuids
ARRAY[2972]::oid[] AS conffeqop, -- 2972 is eq comparison for uuids
ARRAY['uuid_eq'::regproc]::oid[] AS conpfeqop,
ARRAY['uuid_eq'::regproc]::oid[] AS conppeqop,
ARRAY['uuid_eq'::regproc]::oid[] AS conffeqop,
NULL::int2[] AS confdelsetcols,
NULL::oid[] AS conexclop,
NULL::pg_node_tree AS conbin,
Expand All @@ -6889,6 +6894,73 @@ def _generate_sql_information_schema(
JOIN edgedbsql_VER.pg_attribute pa
ON pa.attrelid = pc.oid
AND pa.attname = sl.name || '_id'
UNION ALL
-- foreign keys for:
-- - multi link tables (source & target),
-- - multi property tables (source),
-- - single link with link properties (source & target),
-- these constraints do not actually exist, so we emulate it entierly
SELECT
edgedbsql_VER.uuid_to_oid(sp.id) AS oid,
vt.table_name || '_fk_' || spec.name AS conname,
edgedbsql_VER.uuid_to_oid(vt.module_id) AS connamespace,
'f'::"char" AS contype,
FALSE AS condeferrable,
FALSE AS condeferred,
TRUE AS convalidated,
pc.oid AS conrelid,
pc.reltype AS contypid,
0::oid AS conindid, -- TODO
0::oid AS conparentid,
pcf.oid AS confrelid,
'r'::"char" AS confupdtype,
'r'::"char" AS confdeltype,
's'::"char" AS confmatchtype,
TRUE AS conislocal,
0::int2 AS coninhcount,
TRUE AS connoinherit,
ARRAY[spec.attnum]::int2[] AS conkey,
ARRAY[1]::int2[] AS confkey, -- id will have attnum 1
ARRAY['uuid_eq'::regproc]::oid[] AS conpfeqop,
ARRAY['uuid_eq'::regproc]::oid[] AS conppeqop,
ARRAY['uuid_eq'::regproc]::oid[] AS conffeqop,
NULL::int2[] AS confdelsetcols,
NULL::oid[] AS conexclop,
pc.relpartbound AS conbin,
pc.tableoid,
pc.xmin,
pc.cmin,
pc.xmax,
pc.cmax,
pc.ctid
FROM edgedb_VER."_SchemaPointer" sp
-- find links with link properties
LEFT JOIN LATERAL (
SELECT sl.id
FROM edgedb_VER."_SchemaLink" sl
LEFT JOIN edgedb_VER."_SchemaProperty" AS slp ON slp.source = sl.id
GROUP BY sl.id
HAVING COUNT(*) > 2
) link_props ON link_props.id = sp.id
JOIN pg_class pc ON pc.relname = sp.id::TEXT
JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pc.reltype
-- duplicate each row for source and target
JOIN LATERAL (VALUES
('source', 1::int2, sp.source),
('target', 2::int2, sp.target)
) spec(name, attnum, foreign_id) ON TRUE
JOIN edgedbsql_VER.virtual_tables vtf ON vtf.id = spec.foreign_id
JOIN pg_class pcf ON pcf.reltype = vtf.pg_type_id
WHERE
sp.cardinality = 'Many' OR link_props.id IS NOT NULL
AND sp.computable IS NOT TRUE
AND sp.internal IS NOT TRUE
"""
),
trampoline.VersionedView(
Expand Down
1 change: 1 addition & 0 deletions edb/tools/edb.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ def load_ext(args: tuple[str, ...]):
from . import ast_inheritance_graph # noqa
from . import parser_demo # noqa
from . import ls_forbidden_functions # noqa
from . import redo_metaschema # noqa
from .profiling import cli as prof_cli # noqa
from .experimental_interpreter import edb_entry # noqa
52 changes: 52 additions & 0 deletions edb/tools/redo_metaschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.
#
# 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
#
# 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 edb.tools.edb import edbcommands


@edbcommands.command("redo-metaschema-sql")
def run():
"""
Generates DDL to recreate metaschema for sql introspection.
Can be used to apply changes to metaschema to an existing database.
edb redo-metaschema-sql | ./build/postgres/install/bin/psql \
"postgresql://postgres@/E_main?host=$(pwd)/tmp/devdatadir&port=5432" \
-v ON_ERROR_STOP=ON
"""

from edb.common import devmode
devmode.enable_dev_mode()

from edb.pgsql import dbops, metaschema
from edb import buildmeta

version = buildmeta.get_pg_version()
commands = metaschema._generate_sql_information_schema(version)

for command in commands:
block = dbops.PLTopBlock()

if isinstance(command, dbops.CreateFunction):
command.or_replace = True
if isinstance(command, dbops.CreateView):
command.or_replace = True

command.generate(block)

print(block.to_string())
44 changes: 44 additions & 0 deletions tests/test_sql_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,50 @@ async def test_sql_query_introspection_04(self):
],
)

async def test_sql_query_introspection_05(self):
# test pg_constraint

res = await self.squery_values(
'''
SELECT pc.relname, pcon.contype, pa.key, pcf.relname, paf.key
FROM pg_constraint pcon
JOIN pg_class pc ON pc.oid = pcon.conrelid
LEFT JOIN pg_class pcf ON pcf.oid = pcon.confrelid
LEFT JOIN LATERAL (
SELECT string_agg(attname, ',') as key
FROM pg_attribute
WHERE attrelid = pcon.conrelid
AND attnum = ANY(pcon.conkey)
) pa ON TRUE
LEFT JOIN LATERAL (
SELECT string_agg(attname, ',') as key
FROM pg_attribute
WHERE attrelid = pcon.confrelid
AND attnum = ANY(pcon.confkey)
) paf ON TRUE
WHERE pc.relname IN (
'Book.chapters', 'Movie', 'Movie.director', 'Movie.actors'
)
ORDER BY pc.relname ASC, pcon.contype DESC, pa.key
'''
)

self.assertEqual(
res,
[
['Book.chapters', b'f', 'source', 'Book', 'id'],
['Movie', b'p', 'id', None, None],
['Movie', b'f', 'director_id', 'Person', 'id'],
['Movie', b'f', 'genre_id', 'Genre', 'id'],
['Movie.actors', b'p', 'source,target', None, None],
['Movie.actors', b'f', 'source', 'Movie', 'id'],
['Movie.actors', b'f', 'target', 'Person', 'id'],
['Movie.director', b'p', 'source,target', None, None],
['Movie.director', b'f', 'source', 'Movie', 'id'],
['Movie.director', b'f', 'target', 'Person', 'id'],
],
)

async def test_sql_query_schemas_01(self):
await self.scon.fetch('SELECT id FROM "inventory"."Item";')
await self.scon.fetch('SELECT id FROM "public"."Person";')
Expand Down

0 comments on commit 4b339e5

Please sign in to comment.